d9e5668cec
Fixes and improvements from exhaustive audit: Consistency fixes in SKILL.md: - 'Pipe & Filter' → 'Pipe and Filter' (one stray '&' found and corrected) - 'Singleton for factory instance' → clarified to 'register factory as singleton-scoped via DI container' (less misleading wording) - Documentation Format section updated with note that SKILL.md itself is the authoritative source for related-pattern combinations Coverage fix — Related Patterns sections: - Added '## Related Patterns' to ALL 94 pattern files (was 0/94) - Each section lists 3–6 related patterns with relationship descriptions - Covers: why they're related, when to prefer one vs the other, and which are often confused SOLID principles → Creational → Structural → Behavioral → Architectural → Concurrency → Functional → Resilience → Data Access → Messaging → Testing → Error Handling → Microservice — all 13 categories covered Code verification: - Python: 0 failures (all 85 testable blocks pass) - Go: 0 failures (all 76 testable blocks pass) - JavaScript: 0 failures (all 78 testable blocks pass) - All 239 code blocks verified correct after edits Final skill state: - 108 files, 36,524 lines across 13 reference categories - 94/94 pattern files have Related Patterns sections - 2,815-line SKILL.md with 67 decision trees, 23 scenarios, 0 broken references, 0 naming inconsistencies
8.1 KiB
8.1 KiB
Lazy Loading Pattern
Problem
When loading an entity from the database, eagerly loading all its related entities creates excessive queries and memory usage — especially in deep object graphs. A User with orders, each with line_items, each with product can trigger dozens of queries before any data is actually needed.
Solution
Defer loading of related data until it's actually accessed. The entity holds a placeholder (proxy, thunk, or descriptor) instead of the real data. On first access, the placeholder triggers the actual load. Subsequent accesses return the cached result.
Common variants:
- Virtual proxy — A proxy object that loads the real object on first method call.
- Value holder — A wrapper that holds a loader function and caches the result.
- Ghost — A partially loaded object that fills itself in on first access.
When to Use
- Object graphs with optional or rarely-accessed relations
- APIs where clients request partial data
- Reducing initial query cost when not all fields are needed
- ORM implementations (virtually all ORMs use lazy loading)
When to Avoid
- When you know you'll need the related data (eager load instead)
- In serialization contexts (lazy loads during JSON encoding cause surprises)
- When N+1 query problems are likely (batch loading is better)
- Real-time systems where deferred I/O introduces unpredictable latency
Pseudocode
class Lazy:
loaded = false
value = null
loader = function
function get():
if not loaded:
value = loader()
loaded = true
return value
Python Implementation
from typing import TypeVar, Generic, Callable, Optional
T = TypeVar("T")
class Lazy(Generic[T]):
"""Value holder that defers loading until first access."""
def __init__(self, loader: Callable[[], T]):
self._loader = loader
self._value: Optional[T] = None
self._loaded = False
@property
def value(self) -> T:
if not self._loaded:
self._value = self._loader()
self._loaded = True
return self._value # type: ignore
@property
def is_loaded(self) -> bool:
return self._loaded
class User:
def __init__(self, user_id: int, name: str, order_loader: Callable):
self.id = user_id
self.name = name
self._orders = Lazy(order_loader)
@property
def orders(self):
return self._orders.value
@property
def orders_loaded(self):
return self._orders.is_loaded
class Order:
def __init__(self, order_id: int, total: float):
self.id = order_id
self.total = total
def __repr__(self):
return f"Order(id={self.id}, total=${self.total:.2f})"
# --- Simulated database ---
DB_ORDERS = {
1: [Order(101, 29.99), Order(102, 49.50)],
2: [Order(201, 15.00)],
}
query_count = 0
def load_orders_for(user_id: int):
global query_count
query_count += 1
print(f" [DB] Loading orders for user {user_id} (query #{query_count})")
return DB_ORDERS.get(user_id, [])
# --- Demo ---
print("=== Creating user (orders NOT loaded yet) ===")
user = User(1, "Alice", lambda: load_orders_for(1))
print(f" User: {user.name}, orders_loaded={user.orders_loaded}")
print()
print("=== First access to orders (triggers load) ===")
orders = user.orders
print(f" Orders: {orders}")
print(f" orders_loaded={user.orders_loaded}")
print()
print("=== Second access (cached, no query) ===")
orders_again = user.orders
print(f" Orders: {orders_again}")
print(f" Same list? {orders is orders_again}")
print()
print("=== User 2 — never access orders ===")
user2 = User(2, "Bob", lambda: load_orders_for(2))
print(f" User: {user2.name}, orders_loaded={user2.orders_loaded}")
print()
print(f"Total DB queries: {query_count} (would be 2 with eager loading)")
Output:
=== Creating user (orders NOT loaded yet) ===
User: Alice, orders_loaded=False
=== First access to orders (triggers load) ===
[DB] Loading orders for user 1 (query #1)
Orders: [Order(id=101, total=$29.99), Order(id=102, total=$49.50)]
orders_loaded=True
=== Second access (cached, no query) ===
Orders: [Order(id=101, total=$29.99), Order(id=102, total=$49.50)]
Same list? True
=== User 2 — never access orders ===
User: Bob, orders_loaded=False
Total DB queries: 1 (would be 2 with eager loading)
Go Implementation
package main
import (
"fmt"
"sync"
)
type Lazy[T any] struct {
loader func() T
value T
loaded bool
once sync.Once
}
func NewLazy[T any](loader func() T) *Lazy[T] {
return &Lazy[T]{loader: loader}
}
func (l *Lazy[T]) Get() T {
l.once.Do(func() {
l.value = l.loader()
l.loaded = true
})
return l.value
}
func (l *Lazy[T]) IsLoaded() bool {
return l.loaded
}
// Domain types
type Order struct {
ID int
Total float64
}
func (o Order) String() string {
return fmt.Sprintf("Order(id=%d, total=$%.2f)", o.ID, o.Total)
}
type User struct {
ID int
Name string
orders *Lazy[[]Order]
}
func (u *User) Orders() []Order {
return u.orders.Get()
}
// Simulated DB
var queryCount int
func loadOrders(userID int) []Order {
queryCount++
fmt.Printf(" [DB] Loading orders for user %d (query #%d)\n", userID, queryCount)
db := map[int][]Order{
1: {{101, 29.99}, {102, 49.50}},
2: {{201, 15.00}},
}
return db[userID]
}
func main() {
fmt.Println("=== Creating user (orders NOT loaded yet) ===")
user := &User{
ID: 1,
Name: "Alice",
orders: NewLazy(func() []Order { return loadOrders(1) }),
}
fmt.Printf(" User: %s, orders_loaded=%v\n", user.Name, user.orders.IsLoaded())
fmt.Println()
fmt.Println("=== First access (triggers load) ===")
orders := user.Orders()
fmt.Printf(" Orders: %v\n", orders)
fmt.Println()
fmt.Println("=== Second access (cached) ===")
orders2 := user.Orders()
fmt.Printf(" Orders: %v\n", orders2)
fmt.Println()
fmt.Println("=== User 2 — never access orders ===")
user2 := &User{
ID: 2,
Name: "Bob",
orders: NewLazy(func() []Order { return loadOrders(2) }),
}
fmt.Printf(" User: %s, orders_loaded=%v\n", user2.Name, user2.orders.IsLoaded())
fmt.Printf("\nTotal DB queries: %d\n", queryCount)
}
JavaScript Implementation
class Lazy {
constructor(loader) {
this._loader = loader;
this._value = undefined;
this._loaded = false;
}
get value() {
if (!this._loaded) {
this._value = this._loader();
this._loaded = true;
}
return this._value;
}
get isLoaded() {
return this._loaded;
}
}
// Domain
class User {
constructor(id, name, orderLoader) {
this.id = id;
this.name = name;
this._orders = new Lazy(orderLoader);
}
get orders() {
return this._orders.value;
}
get ordersLoaded() {
return this._orders.isLoaded;
}
}
// Simulated DB
let queryCount = 0;
const DB_ORDERS = {
1: [
{ id: 101, total: 29.99 },
{ id: 102, total: 49.5 },
],
2: [{ id: 201, total: 15.0 }],
};
function loadOrders(userId) {
queryCount++;
console.log(` [DB] Loading orders for user ${userId} (query #${queryCount})`);
return DB_ORDERS[userId] || [];
}
// --- Demo ---
console.log("=== Creating user (orders NOT loaded) ===");
const user = new User(1, "Alice", () => loadOrders(1));
console.log(` User: ${user.name}, ordersLoaded=${user.ordersLoaded}`);
console.log("\n=== First access (triggers load) ===");
console.log(` Orders: ${JSON.stringify(user.orders)}`);
console.log("\n=== Second access (cached) ===");
console.log(` Orders: ${JSON.stringify(user.orders)}`);
console.log(` ordersLoaded=${user.ordersLoaded}`);
console.log("\n=== User 2 — never access orders ===");
const user2 = new User(2, "Bob", () => loadOrders(2));
console.log(` User: ${user2.name}, ordersLoaded=${user2.ordersLoaded}`);
console.log(`\nTotal DB queries: ${queryCount}`);
Related Patterns
- Proxy — a virtual proxy is the most common implementation of lazy loading; the proxy loads the real object on first access.
- Identity Map — lazy loading must check the identity map before issuing a query to avoid loading the same entity twice.
- Repository — repositories trigger lazy loading when related collections are first accessed.