Common Storage Implementation Mistakes
Learn from real-world implementation issues and how to avoid them
Common Storage Implementation Mistakes
💡 RECOMMENDED APPROACHES
For features needing backend persistence (Session 166.3):
// ✅ RECOMMENDED: autoFlashState + syncToBackend
const prefs = autoFlashState('user-prefs', { theme: 'light' });
syncToBackend(prefs, { descriptorKey: 'user-prefs', debounceMs: 1000 });
// Instant local + durable backend with retry resilience
See BackendSyncEngine Guide for full setup.
For local-only features or dev-mode API simulation:
// ✅ ALTERNATIVE: HTTP + Mock API (dev-mode, API-ready)
utils.configureMockInterceptor({
persistence: { storage: 'intelligent', namespace: 'api-cache' }
}, mockDefinitions);
🚨 Common Mistake #0: Wrong Storage Type Selection
The Problem
Using inappropriate storage types for your use case leads to poor performance or lost data.
// ❌ WRONG: Using memory for production API caching
persistence: {
storage: 'memory' // ← Data lost on refresh!
}
// ❌ WRONG: Using localStorage for large theme data
persistence: {
storage: 'localstorage' // ← Limited to 5-10MB, will fail!
}
// ❌ WRONG: Using intelligent for unit tests
persistence: {
storage: 'intelligent' // ← Persists across tests, contaminated state!
}
The Solution: Match Storage Type to Use Case
✅ Production Features → 'intelligent'
// ✅ CORRECT: Production API caching with persistence
persistence: {
storage: 'intelligent', // Smart optimization + redundancy
namespace: 'api-cache',
ttl: 3600000 // 1 hour
}
✅ Unit Tests → 'memory'
// ✅ CORRECT: Clean state per test
beforeEach(() => {
configureMockInterceptor({
persistence: {
storage: 'memory', // Fresh state each test
ttl: 0
}
});
});
✅ Demo Presentations → 'memory'
// ✅ CORRECT: Fresh data each demo session
persistence: {
storage: 'memory', // No persistence between demos
ttl: 3600000 // 1 hour demo session
}
✅ Storage Debugging → 'localstorage'
// ✅ CORRECT: Force specific adapter for debugging
persistence: {
storage: 'localstorage', // Force localStorage for isolation
autoCleanup: false // Keep data for inspection
}
Quick Reference
| Use Case | Storage Type | Why |
|---|---|---|
| Production features | 'intelligent' |
Smart optimization, redundancy |
| Unit tests | 'memory' |
Clean state, no side effects |
| Demo presentations | 'memory' |
Fresh data each session |
| Storage debugging | 'localstorage' |
Predictable, inspectable |
| Legacy migration | 'indexeddb' |
Match old DB structure |
🚨 CRITICAL DISCOVERY: Mock Storage Usage
We discovered the root cause of all storage system issues: Mock Storage Usage
When provideGgStorage(config) is called WITHOUT a factory function, it returns a mock implementation that:
- ✅ Returns
truefor allput()operations (lies about success) - ❌ Actually saves nothing
- ❌ Returns
[]for alllist()operations - ❌ Returns
nullfor allget()operations
This explains why:
- Data appears to save successfully but disappears after refresh
- All diagnostic and recovery attempts fail
- Configuration is ignored (working with mock instance)
🚨 Critical Mistake #1: Mock Storage Factory (Direct Storage)
The Problem
Most common cause of "data doesn't persist" issues.
When provideGgStorage is called without a storage factory, it returns a mock implementation that appears to work but doesn't save anything:
// ❌ WRONG: Creates mock storage that doesn't save
...provideGgStorage({
namespace: 'my-app',
strategy: 'balanced'
})
// Missing second parameter → Mock implementation used
The Symptoms
// Mock storage behavior:
await storage.put('key', data); // ✅ Returns true (lies about success)
await storage.get('key'); // ❌ Returns null (nothing saved)
await storage.list(); // ❌ Returns [] (empty)
Log patterns that indicate mock storage:
✅ PUT "user/data" completed in 0ms, success: true // ← Suspiciously instant
❌ All keys found: 0 [] // ← Nothing in storage
❌ Successfully saved theme to storage: theme-xyz // ← Claims success
❌ Theme keys after filtering: 0 // ← But finds nothing
The Solution: Type-Safe Wrapper Pattern
// ✅ CORRECT: Always provide real storage factory with type safety
import { GgStorage as CoreGgStorage } from 'libs/storage-core/src/lib/gg-storage';
import type { GgStorage, GgStorageConfig } from 'libs/storage-angular/src/lib/tokens';
// Create type-safe wrapper that bridges storage-core and storage-angular
const createStorageWrapper = (config: GgStorageConfig): GgStorage => {
const coreStorage = new CoreGgStorage(config);
return {
get: (key: string) => coreStorage.get(key),
put: (key: string, value: any) => coreStorage.put(key, value),
remove: (key: string) => coreStorage.remove(key),
has: (key: string) => coreStorage.has(key),
list: (options?: any) => coreStorage.list(options),
clear: (prefix?: string) => coreStorage.clear(prefix),
getConfig: () => config,
onEvent: (listener: (event: any) => void) => coreStorage.onEvent(listener),
getAdapterKind: () => coreStorage.getAdapterKind?.() || 'localStorage',
switchAdapter: (kind: 'opfs' | 'dexie' | 'localStorage') => coreStorage.switchAdapter(kind)
} as GgStorage;
};
...provideGgStorage(config, createStorageWrapper) // 🔑 CRITICAL: Real factory with types!
🚨 Critical Mistake #2: Adapter Interference
The Problem
Manual storage adapter switching disrupts intelligent storage decisions.
Our theme system encountered this exact issue:
// ❌ WRONG: Manual adapter switching
const storage = inject(StorageService);
// App component automatically switches adapters on errors:
if (errorDetected) {
storage.switchAdapter('localStorage'); // ← Breaks theme persistence!
}
// Results in:
// 1. Themes saved to OPFS/IndexedDB
// 2. Error triggers adapter switch to localStorage
// 3. App queries localStorage → finds nothing
// 4. "All themes lost after refresh"
The Symptoms
🧠 PUT Decision for "themes/data/theme-123": 💾 OPFS storage...
⚡ PUT "themes/data/theme-123" completed in 22ms, success: true
[Later, after adapter switch...]
🔍 ENHANCED STORAGE: All keys found: 0 []
🔍 ENHANCED STORAGE: No theme keys found
The Solution
// ✅ CORRECT: Trust the intelligent system
const storage = inject(StorageService);
// DON'T manually switch adapters - the system handles failures automatically
// The intelligent storage system provides:
// - Automatic adapter selection based on data characteristics
// - Smart redundancy for critical data
// - Automatic failover when adapters fail
// - Performance optimization across adapters
// If you must check capabilities:
const capabilities = storage.getCapabilities();
if (capabilities.opfs) {
console.log('OPFS available for large data storage');
}
🚨 Critical Mistake #3: Multiple Storage Instances
The Problem
Creating multiple storage configurations leads to data isolation.
// ❌ WRONG: Multiple storage instances
export const appConfig: ApplicationConfig = {
providers: [
// Different namespaces create separate storage instances
...provideGgStorage({ namespace: 'themes' }, factory),
...provideGgStorage({ namespace: 'user-data' }, factory),
...provideGgStorage({ namespace: 'cache' }, factory),
// Feature also provides its own storage
...provideFeatureStorage({ namespace: 'feature' })
]
};
The Symptoms
- Data saved in one context not visible in another
- Inconsistent behavior between components
- Multiple IndexedDB databases created
- Themes/data "disappear" when switching contexts
The Solution
// ✅ CORRECT: Single storage instance with semantic keys
export const appConfig: ApplicationConfig = {
providers: [
// Single unified storage instance
...provideGgStorage({
namespace: 'unified-app', // Single namespace for entire app
strategy: 'balanced'
}, (config) => new GgStorage(config)),
// Use semantic keys to separate concerns:
// - 'themes/data/theme-123'
// - 'user/profile/user-456'
// - 'cache/api/users'
// - 'feature/dashboard/config'
]
};
🚨 Critical Mistake #4: Legacy Service Conflicts
The Problem
Old storage services running alongside new ones create conflicts.
Our theme system had this exact issue:
// ❌ WRONG: Both old and new storage services active
@Injectable({ providedIn: 'root' })
export class OldStorageService { // ← Still creates IndexedDB databases
// ... old implementation
}
@Injectable({ providedIn: 'root' })
export class NewStorageService { // ← Uses intelligent storage
// ... new implementation
}
// Services inject different storage services:
export class FeatureService {
private oldStorage = inject(OldStorageService); // ← Wrong!
private newStorage = inject(NewStorageService); // ← Conflict!
}
The Symptoms
- Multiple IndexedDB databases created (
theme-system-db,intelligent-themesStorageDb) - Data saved to one database but queries look in another
- "Data exists but not found" behavior
- Themes persist initially but disappear after refresh
The Solution
// ✅ CORRECT: Disable old services, update injections
@Injectable({
providedIn: null // ← Disable old service
})
export class LegacyStorageService { /* ... */ }
@Injectable({ providedIn: 'root' })
export class EnhancedStorageService { /* ... */ }
// Update all injections to use new service:
export class FeatureService {
private storage = inject(EnhancedStorageService); // ← Single source of truth
}
🚨 Common Mistake #5: Blocking Operations
The Problem
Storage operations are asynchronous but treated as synchronous.
// ❌ WRONG: Treating async operations as sync
const userData = storage.get('user/profile'); // Returns Promise<any>, not data!
console.log(userData.name); // Error: undefined
// ❌ WRONG: Not awaiting operations
storage.put('user/data', data); // Promise not awaited
router.navigate('/next-page'); // Navigation before save completes
The Solution
// ✅ CORRECT: Proper async/await patterns
const userData = await storage.get('user/profile');
console.log(userData?.name);
// ✅ BETTER: Use reactive signals for UI
const userData = storage.asSignal('user/profile', null).value;
// Automatically updates when data changes
// ✅ CORRECT: Await operations before navigation
await storage.put('user/data', data);
router.navigate('/next-page');
🚨 Common Mistake #6: Ignoring Key Patterns
The Problem
Generic key names prevent intelligent optimization.
// ❌ WRONG: Generic keys don't help system optimize
await storage.put('data1', userData); // No intelligence possible
await storage.put('config', appConfig); // Can't determine importance
await storage.put('cache', cacheData); // No optimization hints
The Solution
// ✅ CORRECT: Semantic keys enable intelligence
await storage.put('user/profile', userData); // → Critical: Redundant storage
await storage.put('settings/app/config', appConfig); // → User-pref: Fast access
await storage.put('cache/api/response', cacheData); // → Cache: Structured storage
🚨 Common Mistake #7: Not Handling Storage Events
The Problem
Missing cross-tab synchronization and real-time updates.
// ❌ WRONG: Static data without reactivity
export class UserService {
private userData: any = null;
async loadUser() {
this.userData = await storage.get('user/profile');
}
getUserData() {
return this.userData; // Stale data if changed in another tab
}
}
The Solution
// ✅ CORRECT: Reactive signals with automatic updates
export class UserService {
private storage = inject(StorageService);
// Automatically syncs across tabs and updates UI
readonly userData = this.storage.asSignal('user/profile', null).value;
// Manual refresh when needed
async refreshUserData() {
const { refresh } = this.storage.asSignal('user/profile', null);
await refresh();
}
}
🔍 Debugging Checklist
When storage isn't working, check these common issues:
✅ Verify Real Storage Factory
// Check your app.config.ts has the factory function:
...provideGgStorage(config, (cfg) => new GgStorage(cfg))
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^ Must be present!
✅ Check for Adapter Interference
# Search for manual adapter switching:
grep -r "switchAdapter\|switchNamespace" src/
# Remove any manual switching unless absolutely necessary
✅ Verify Single Storage Instance
// Only ONE provideGgStorage call per app:
export const appConfig: ApplicationConfig = {
providers: [
...provideGgStorage({ namespace: 'single-app' }, factory), // ← Only one!
// No other storage providers
]
};
✅ Check Service Injections
# Ensure all services use the same storage service:
grep -r "inject.*StorageService" src/
# All should inject the same enhanced storage service
✅ Monitor Console Logs
# Healthy patterns:
✅ "🧠 PUT Decision for..." (real intelligent decisions)
✅ "⚡ GET...from memory in 0.10ms" (proper caching)
✅ "💾 OPFS direct write..." (real storage operations)
# Problem patterns:
❌ "All keys found: 0 []" (mock storage or conflicts)
❌ "success: true" in 0ms (mock storage)
❌ "No theme keys found" after "Successfully saved" (conflicts)
🚨 Common Mistake #8: Loading Backend Data Without Hydration Gate (Session 172)
The Problem
Loading backend data before autoFlashState hydrates causes stale data flash.
// ❌ WRONG: Backend loads before IndexedDB hydration → operates on DEFAULT_STATE
constructor() {
effect(() => {
const userId = this._userId();
if (!userId) return;
this.loadFromBackend(); // Fires immediately — IndexedDB may not have loaded yet!
});
}
The Solution: Hydration Gate + Write INTO autoFlashState
// ✅ CORRECT: Gate backend loading on hydration, write INTO autoFlashState
private readonly state = autoFlashState<MyState>('my-key', defaultState, config);
// state.hydrated is a Signal<boolean> — true when IndexedDB loaded (Session 172)
constructor() {
effect(() => {
if (!this.state.hydrated()) return; // Wait for cache
const userId = this._userId();
if (!userId) return;
this.loadFromBackend(); // Now safe — cached data is in state
});
}
private loadFromBackend(): void {
this.http.get<BackendData>(url).subscribe(data => {
// Write backend data INTO autoFlashState — atomic merge, no flash
this.state.update(s => ({
...s,
items: this.mergeBackendData(s.items, data),
}));
});
}
Key points:
state.hydrated(Session 172) gates backend loading on IndexedDB hydration- Backend data is written INTO autoFlashState, not a separate signal
- The merge is atomic — no flash, no stale-then-remove cycle
- IndexedDB is automatically updated (autoFlashState persists on change)
- Next page load: IndexedDB has backend-validated data
See: BackendSyncEngine Guide for full pattern documentation.
🚨 Common Mistake #9: In-Memory Retry Queue (Session 166.3)
The Problem
Retry queues in plain variables are lost on page refresh — the exact moment sync failures matter most.
// ❌ WRONG: In-memory queue — lost on page refresh
private retryQueue: PendingOp[] = [];
onFailure(op: PendingOp) {
this.retryQueue.push(op); // Gone if user refreshes!
}
The Solution: Persistent Retry Queue
// ✅ CORRECT: Retry queue persisted in IndexedDB via autoFlashState
private readonly retryQueue = autoFlashState<PendingOp[]>('my-retry-queue', [], {
database: 'sync', collection: 'retry-queue', priority: 'critical'
});
// Queue survives page refresh
// NetworkSyncStateService.onBackendAvailable$ → drain on reconnect
// BackendSyncEngine handles this automatically — use it instead of DIY
🔧 Emergency Recovery
If you encounter persistent issues:
Clear All Storage Data
// ⚠️ Nuclear option: Clear all app data
await storage.clear(); // Clears all data for current namespace
Reset to Default Configuration
// Reset storage to defaults
storage.setStrategy('balanced');
storage.setSyncAdapter(null);
// Clear browser storage manually if needed
localStorage.clear();
// Clear IndexedDB via browser DevTools → Application → Storage
Force Specific Adapter (Debug Only)
// 🔧 DEBUG ONLY: Force specific adapter for testing
const success = storage.switchAdapter('localStorage');
console.log('Adapter switch result:', success);
// Remember to trust the intelligence in production!
📊 Success Metrics
Your storage implementation is working correctly when you see:
✅ Performance Logs
🧠 Intelligent Storage v2 initialized: {strategy: 'balanced', adapter: 'dexie'}
⚡ GET "user/data" from memory in 0.10ms
💾 OPFS direct write using temporary adapter: themes/data/theme-xyz
📦 Micro-cached: user/settings
✅ Data Persistence
- Data survives page refresh
- Data syncs across browser tabs
- Multiple themes/items persist correctly
- No "All keys found: 0" errors
✅ Intelligent Decisions
🧠 PUT Decision for "user/profile": 💾 OPFS storage + 🔍 Dexie index
🧠 PUT Decision for "cache/api": 🔍 Dexie inline storage
📦 Micro-cache hit: frequently-accessed-data
✅ Best Practices (from Flash Persistence)
- Use descriptive, hierarchical keys —
user-data:preferences:*,app-data:cache:* - Use
computed()for derived state — Avoid recomputing on every access - Batch updates — Single
cart.update(c => [...c, ...items])instead of multipleupdate()calls - Avoid mutating state directly —
cart().push(item)doesn't trigger persistence; usecart.update(items => [...items, item]) - Use TTL for cache/temporary data —
ttl: 5 * 60 * 1000for 5-minute cache - Never store sensitive data (passwords, credit cards) in browser storage
🎯 Migration Recommendation
For New Features
✅ Use HTTP + Mock API pattern with storage: 'intelligent' for API-readiness
For Existing Features
✅ Consider migrating to HTTP + Mock API for API-readiness
✅ If keeping direct storage, ensure real factory is provided
Next Steps:
- Getting Started — 5-minute setup
- BackendSyncEngine — Fire-and-forget backend sync (Session 166.3)
- API Reference — Complete API