Composite DataSource
Layered datasource composition for cache, persistence, and replica stacking with read-through, write-through, write-behind, and refresh-ahead strategies
Source files
DataManagementModelsStandard/Composite/CompositeLayer.csDataManagementModelsStandard/Composite/ILayer.csDataManagementModelsStandard/Composite/ICompositeLayerDataSource.cs
Table of Contents
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
| Strategy | Read Behavior | Write Behavior | Consistency |
|---|---|---|---|
| Read-Through | Cache miss → load from primary → populate cache | N/A | Eventual |
| Write-Through | N/A | Write primary → update cache | Strong |
| Write-Behind | N/A | Write cache → async write to primary | Weak |
| Refresh-Ahead | Proactively refresh before expiry | N/A | Near real-time |
| Cache-Aside | App checks cache first, loads if miss | App writes primary → invalidates cache | Strong |
Layer Strategies
| Strategy | Description | Use Case |
|---|---|---|
CacheOnly | Reads from cache only, no fallback | Stale data OK, high performance |
CacheThenPrimary | Cache first, fallback to primary on miss | Most common pattern |
PrimaryOnly | Skip cache, go direct to primary | Always-fresh data needed |
PrimaryThenReplica | Primary first, replica on failure | High availability reads |
AllAvailable | Try all layers, first success wins | Maximum 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
| Policy | Description | Best For |
|---|---|---|
| TTL | Time-to-live per entry | Frequently updated data |
| LRU | Least recently used | Session data, recent items |
| LFU | Least frequently used | Popular content |
| Size-based | Max item count | Memory-constrained |
| Sliding | Reset TTL on access | Active 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