Summary
DialCacheKeyConfig is starting to outgrow the current flat / property-first shape. staleOnErrorMaxAgeSec and remoteReadTimeoutMs are specifically Redis-layer behavior, while ttlSec and ramp are currently expressed as cross-layer maps.
As layer capabilities diverge, the public API would likely be easier to understand if policy were grouped by layer first, with remote-only behavior nested under remote.
This should be a follow-up to stale-on-error rather than part of PR #121.
Current shape
new DialCacheKeyConfig({
ttlSec: {
[CacheLayer.LOCAL]: 60,
[CacheLayer.REMOTE]: 300,
},
ramp: {
[CacheLayer.LOCAL]: 50,
[CacheLayer.REMOTE]: 100,
},
staleOnErrorMaxAgeSec: 3_600,
remoteReadTimeoutMs: 50,
requestLocal: true,
coalesce: true,
shadow: {
ramp: 1,
},
});
This organizes config by property first and then layer. That works while layers are mostly symmetric, but the matrix is already diverging:
local remote
TTL ✓ ✓
ramp ✓ ✓
read timeout ✗ ✓
stale-on-error ✗ ✓
invalidation ✗ ✓
shadow validation ✗ ✓
Proposed direction
Prefer layer-owned policy objects:
new DialCacheKeyConfig({
local: {
ttlSec: 60,
ramp: 50,
},
remote: {
ttlSec: 300,
ramp: 100,
readTimeoutMs: 50,
staleOnError: {
maxAgeSec: 3_600,
},
shadow: {
ramp: 1,
},
},
requestLocal: true,
coalesce: true,
});
Conceptually:
interface DialCacheKeyConfig {
local?: LocalCachePolicy;
remote?: RemoteCachePolicy;
requestLocal?: boolean;
coalesce?: boolean;
}
interface LocalCachePolicy {
ttlSec?: number;
ramp?: number;
}
interface RemoteCachePolicy {
ttlSec?: number;
ramp?: number;
readTimeoutMs?: number;
staleOnError?: {
maxAgeSec: number;
};
shadow?: {
ramp?: number;
logMismatches?: boolean;
};
}
This makes related Redis behavior read together naturally:
remote: {
ttlSec: 300,
ramp: 100,
readTimeoutMs: 50,
staleOnError: {
maxAgeSec: 3_600,
},
}
It also leaves room for future remote-only behavior without continuing to add top-level fields, for example:
remote: {
staleOnError: {
maxAgeSec: 3_600,
// possible future policy, if needed
strategy: "retain",
},
}
or:
remote: {
staleOnError: {
maxAgeSec: 3_600,
ramp: 25,
},
}
Keep application behavior separate from runtime cache policy
I would not move shouldAttemptStaleRecovery into DialCacheKeyConfig.
That option describes application semantics — which source errors are safe to degrade on — rather than operational rollout policy. It fits better alongside stable use-case behavior such as shadowComparator:
const getUser = dialcache.cached(fetchUser, {
keyType: "user_id",
useCase: "GetUser",
cacheKey: (id) => id,
shouldAttemptStaleRecovery: isTransientDatabaseError,
defaultConfig: new DialCacheKeyConfig({
remote: {
ttlSec: 300,
staleOnError: {
maxAgeSec: 3_600,
},
},
}),
});
That gives a useful conceptual split:
use-case behavior
├── cacheKey
├── serializer
├── coalesce
├── shouldAttemptStaleRecovery
└── shadowComparator
runtime cache policy
├── requestLocal
├── local
│ ├── ttl
│ └── ramp
└── remote
├── ttl
├── ramp
├── readTimeout
├── staleOnError
└── shadow
Open question: where should shadow live?
shadow is not purely a Redis primitive; it is a higher-level validation mechanism that happens to operate against the remote layer. From a user mental-model perspective, though, it is remote-cache behavior.
Two reasonable options:
remote: {
shadow: { ... },
}
or keep it top-level:
new DialCacheKeyConfig({
remote: { ... },
shadow: { ... },
});
My preference is remote.shadow, but this is worth deciding explicitly.
Migration considerations
This is a public API refactor and should not be bundled into stale-on-error itself.
Potential migration paths:
- Breaking replacement in the next intentional config-schema break.
- Temporarily accept both forms, reject ambiguous mixed configuration, and normalize internally.
- Add the layer-first form first, deprecate the old
ttlSec / ramp maps and remote-specific top-level fields, then remove them in a later release.
If both forms coexist temporarily, configuration precedence should not be implicit. A config such as this should probably throw rather than guess:
new DialCacheKeyConfig({
ttlSec: { [CacheLayer.REMOTE]: 60 },
remote: { ttlSec: 300 },
});
Motivation
The goal is not nesting for its own sake. The existing shape is optimized for symmetric layers, while DialCache's layers increasingly expose different capabilities. Grouping by layer should:
- make configuration more discoverable;
- keep remote-only features from accumulating at the top level;
- make validation and documentation easier to reason about;
- provide a cleaner home for future remote policy;
- make configs read in the same hierarchy users use to think about the cache chain.
Related: PR #121 (stale-on-error recovery).
Summary
DialCacheKeyConfigis starting to outgrow the current flat / property-first shape.staleOnErrorMaxAgeSecandremoteReadTimeoutMsare specifically Redis-layer behavior, whilettlSecandrampare currently expressed as cross-layer maps.As layer capabilities diverge, the public API would likely be easier to understand if policy were grouped by layer first, with remote-only behavior nested under
remote.This should be a follow-up to stale-on-error rather than part of PR #121.
Current shape
This organizes config by property first and then layer. That works while layers are mostly symmetric, but the matrix is already diverging:
Proposed direction
Prefer layer-owned policy objects:
Conceptually:
This makes related Redis behavior read together naturally:
It also leaves room for future remote-only behavior without continuing to add top-level fields, for example:
or:
Keep application behavior separate from runtime cache policy
I would not move
shouldAttemptStaleRecoveryintoDialCacheKeyConfig.That option describes application semantics — which source errors are safe to degrade on — rather than operational rollout policy. It fits better alongside stable use-case behavior such as
shadowComparator:That gives a useful conceptual split:
Open question: where should shadow live?
shadowis not purely a Redis primitive; it is a higher-level validation mechanism that happens to operate against the remote layer. From a user mental-model perspective, though, it is remote-cache behavior.Two reasonable options:
or keep it top-level:
My preference is
remote.shadow, but this is worth deciding explicitly.Migration considerations
This is a public API refactor and should not be bundled into stale-on-error itself.
Potential migration paths:
ttlSec/rampmaps and remote-specific top-level fields, then remove them in a later release.If both forms coexist temporarily, configuration precedence should not be implicit. A config such as this should probably throw rather than guess:
Motivation
The goal is not nesting for its own sake. The existing shape is optimized for symmetric layers, while DialCache's layers increasingly expose different capabilities. Grouping by layer should:
Related: PR #121 (stale-on-error recovery).