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:

This explains why:

🚨 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

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

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:

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

✅ 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)


🎯 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: