Source files
  • DataManagementModelsStandard/Composite/CompositeLayer.cs
  • DataManagementModelsStandard/Composite/ILayer.cs
  • DataManagementModelsStandard/Composite/ICompositeLayerDataSource.cs

Overview

The Composite DataSource pattern allows stacking multiple datasources into prioritized layers for automatic data propagation:

  • Cache layer — Fast in-memory reads, transparently populated from lower layers
  • Primary layer — Main persistent storage (read/write, source of truth)
  • Replica layer — Read replicas for scaling (read-only, eventual consistency)
  • Fallback layer — Last-resort data source when all above fail

Architecture & Patterns

CompositeLayerDataSource
├── Layer 1: Cache       (Read-Through, Write-Through)
│   └── InMemoryDataSource / CachedMemoryDataSource
├── Layer 2: Primary     (Read-Through, Write-Through)
│   └── SqlServerDataSource / PostgreSQLDataSource
├── Layer 3: Replica     (Read-Through, Read-Only)
│   └── ReadReplicaDataSource
└── Layer 4: Fallback    (Read-Through, Read-Only)
    └── BackupDataSource

Layer Order

Layers are tried in registration order (first = highest priority). Place cache layers first, then primary, then fallbacks.

CompositeLayer Class

public class CompositeLayer : ILayer
{
    public CompositeLayer();
    public CompositeLayer(string layerName, string dataViewDataSourceName,
        string localDBDriver, string localDBDriverVersion);

    public int ID { get; set; }
    public string GuidID { get; set; }
    public string LayerName { get; set; }
    public DateTime DateCreated { get; set; }
    public DateTime DateUpdated { get; set; }
    public IDataSource DataSource { get; set; }
    public List<EntityStructure> Entities { get; set; }
    public string DataSourceName { get; set; }
    public string DataViewDataSourceName { get; set; }
    public string LocalDBDriver { get; set; }
    public string LocalDBDriverVersion { get; set; }
}

Caching Strategies

StrategyRead BehaviorWrite BehaviorConsistency
Read-ThroughCache miss → load from primary → populate cacheN/AEventual
Write-ThroughN/AWrite primary → update cacheStrong
Write-BehindN/AWrite cache → async write to primaryWeak
Refresh-AheadProactively refresh before expiryN/ANear real-time
Cache-AsideApp checks cache first, loads if missApp writes primary → invalidates cacheStrong

Layer Strategies

StrategyDescriptionUse Case
CacheOnlyReads from cache only, no fallbackStale data OK, high performance
CacheThenPrimaryCache first, fallback to primary on missMost common pattern
PrimaryOnlySkip cache, go direct to primaryAlways-fresh data needed
PrimaryThenReplicaPrimary first, replica on failureHigh availability reads
AllAvailableTry all layers, first success winsMaximum resilience

Read Path

// 1. Try cache
var cacheResult = cacheLayer.GetEntity("Customers", filters);
if (cacheResult != null) return cacheResult;

// 2. Cache miss → try primary
var primaryResult = primaryLayer.GetEntity("Customers", filters);
if (primaryResult != null)
{
    // 3. Populate cache (read-through)
    cacheLayer.InsertEntity("Customers", primaryResult);
    return primaryResult;
}

// 4. Primary failed → try replica
var replicaResult = replicaLayer.GetEntity("Customers", filters);
if (replicaResult != null) return replicaResult;

// 5. All failed
return null;

Write Path

// Write-Through: Primary first, then cache
var result = primaryLayer.InsertEntity("Customers", entity);
if (result.Flag != Errors.Ok) return result;
cacheLayer.UpdateEntity("Customers", entity);

// Write-Behind: Cache first, async primary
cacheLayer.InsertEntity("Customers", entity);
await _writeBehindQueue.EnqueueAsync(() =>
    primaryLayer.InsertEntity("Customers", entity));

Eviction Policies

PolicyDescriptionBest For
TTLTime-to-live per entryFrequently updated data
LRULeast recently usedSession data, recent items
LFULeast frequently usedPopular content
Size-basedMax item countMemory-constrained
SlidingReset TTL on accessActive sessions
// Configure eviction
composite.WithCacheExpiration(TimeSpan.FromMinutes(30));   // TTL
composite.WithMaxCacheSize(10000);                          // Size-based
composite.WithEvictionPolicy(EvictionPolicy.LRU);           // LRU

Health & Monitoring

// Health checks
composite.WithHealthCheckInterval(TimeSpan.FromSeconds(30));

// Layer health events
composite.OnLayerDown += (s, e) =>
    Console.WriteLine($"Layer {e.LayerName} is down");

composite.OnLayerRestored += (s, e) =>
    Console.WriteLine($"Layer {e.LayerName} restored");

// Hit/miss statistics
var stats = composite.GetCacheStats();
Console.WriteLine($"Hit ratio: {stats.HitRatio:P1}");
Console.WriteLine($"Misses: {stats.MissCount}");
Console.WriteLine($"Evictions: {stats.EvictionCount}");
Console.WriteLine($"Avg read latency: {stats.AvgReadLatencyMs:F1}ms");

Common Patterns

Read-Through Cache

composite.AddLayer(new CompositeLayer
    { LayerName = "L1_Cache", DataSource = cacheDs });
composite.AddLayer(new CompositeLayer
    { LayerName = "L2_Primary", DataSource = primaryDs });
composite.WithReadStrategy(ReadStrategy.CacheThenPrimary);

Write-Through Cache

composite.WithWriteStrategy(WriteStrategy.WriteThrough);

Write-Behind (Async)

composite.WithWriteStrategy(WriteStrategy.WriteBehind);
composite.WithWriteBehindQueueSize(1000);
composite.WithWriteBehindInterval(TimeSpan.FromSeconds(5));

Multi-Tier with Fallback

composite.AddLayer(new CompositeLayer
    { LayerName = "L1_Redis", DataSource = redisDs });
composite.AddLayer(new CompositeLayer
    { LayerName = "L2_Primary", DataSource = sqlDs });
composite.AddLayer(new CompositeLayer
    { LayerName = "L3_Fallback", DataSource = backupDs });
composite.WithFallbackStrategy(FallbackStrategy.TryNextOnFailure);

Cache Stampede Prevention

// Prevent multiple simultaneous cache-loads for same key
composite.WithCacheStampedeProtection(
    maxConcurrentLoads: 5,
    lockTimeout: TimeSpan.FromSeconds(10));

// Probabilistic early expiration to smooth reloads
composite.WithEarlyExpiration(factor: 0.8); // Refresh at 80% TTL

Usage Examples

var cacheDs = new InMemoryCacheDataSource(cacheProps, editor);
var primaryDs = editor.GetDataSource("ProductionDB");
var replicaDs = editor.GetDataSource("ReadReplica");

var composite = new CompositeLayerDataSource();
composite.AddLayer(new CompositeLayer
    { LayerName = "Cache", DataSource = cacheDs });
composite.AddLayer(new CompositeLayer
    { LayerName = "Primary", DataSource = primaryDs });
composite.AddLayer(new CompositeLayer
    { LayerName = "Replica", DataSource = replicaDs });

composite.WithReadStrategy(ReadStrategy.CacheThenPrimary);
composite.WithWriteStrategy(WriteStrategy.WriteThrough);
composite.WithCacheExpiration(TimeSpan.FromMinutes(30));

var customers = composite.GetEntity("Customers", filters);
composite.InsertEntity("Customers", newCustomer);

Pitfalls

  • Stale reads — Cache may return old data; use short TTL for frequently updated entities
  • Write inconsistency — WriteBehind is fast but can lose data on crash; use WriteThrough for critical data
  • Memory pressure — Cache layer grows unbounded without eviction policy; set max size
  • Layer mismatch — All layers must support the same entity schema; validate on setup
  • FanOut cost — Sending reads to all layers increases load; use only when necessary
  • Cache stampede — Multiple concurrent cache misses load same key simultaneously; use stampede protection
  • Double writing — WriteThrough writes to both cache and primary synchronously; use WriteBehind for throughput