Part of #115 (userver-vs-morph survey).
Observation
morph has no caching framework — every "cache" hit in include/morph/ is either an unrelated code comment or a function-local static const std::string memoizing a schema string, not a TTL/eviction/cache-aside abstraction. userver bundles cache::CacheUpdateTrait/components::CachingComponentBase with full vs. incremental update modes and cache dumps — persisting a snapshot to survive a failed first cache load on cold start.
Status: speculative, not a confirmed gap
morph has no in-memory read-cache layer today, so there's nothing to add TTL/eviction semantics to yet.
Revisit only if
morph grows an in-memory read-cache layer in front of Lightweight. If it does, the cache-dump idea (persist last-known-good state so a cold start with a broken dependency still boots) is worth considering — it rhymes with SqliteOfflineQueue's durability goal, though it's not directly applicable without a caching framework to attach it to.
Reference
Full comparison: #115, §3.7 and §4 of the attached document (docs/superpowers/findings/userver-vs-morph-2026-08-17.md).
Deep analysis: #119 caching framework
Verification. The prior investigation's findings all hold on current source, and kanban (PR #121) sharpens rather than overturns them. bank::AccountModel remains the only hand-rolled read cache: examples/bank/include/bank/db/row_versions.hpp (a ~40-line version-counter singleton) plus a per-instance memo (_row/_loadedId/_seenVersion, examples/bank/include/bank/models/account_model.hpp) whose lock-free correctness comes from strand serialization. On ladder-kanban-impl, BoardModel::buildState re-queries small indexed tables per call and the hot poll path GetEventsSince is DB-incremental (WHERE id > lastEventId). The one growing cost is GetActivity: it calls FileActionLog::entries() (full file re-read + re-parse, include/morph/journal/file_action_log.hpp:201-233), and the GUI triggers it on every applied board event (board_qml_bridge.cpp::onEventApplied) — O(journal size) per event. Kanban documents this inline as acceptable at rung scale, and rotate() already provides the retention seam.
Design assessment. If the trigger ever fires, the right shape is not userver's CacheUpdateTrait (TTL/eviction/dump lifecycle) but a generalization of what bank already proved: a header-only VersionedCache split the way bank splits it — a locked VersionRegistry<K> (writers bump(key)) plus a per-instance, lock-free Versioned<V> memo (getOrLoad(key, registry, loader)), living in include/morph/util/ beside datetime.hpp/quantity.hpp with a docs/spec/util/ page. TTL and eviction stay out: strand-serialized instances make explicit version stamps strictly better than time-based staleness, and morph's journal already provides natural version sources (LogEntry::seq, kanban's monotonic BoardEventRecord::id — a cached activity view could invalidate off the same cursor GetEventsSince uses). The cache-dump idea stays inapplicable: morph clients' cold-start durability is the local SQLite file plus SqliteOfflineQueue — the authority survives restart, so there is no "last known good snapshot" distinct from the store itself. Server-side, docs/spec/core/shared_instances.md deliberately solves the superset (cross-attacher divergence), and examples/IMPLEMENTATION.md:138 rightly forbids per-rung cache layers.
Cost/benefit. Helper: S (~50-100 lines) but with real carrying cost — Doxygen-gated public API, a spec file, tests — for zero current consumers. Framework: M-L, all risk (staleness is exactly the bug class bank's pattern exists to prevent) and no beneficiary.
Recommendation: defer, unchanged, with sharpened triggers. Revisit when (a) kanban's GetActivity shows measured cost at realistic journal sizes despite rotate(), (b) a rung needs cross-instance read state shared instances can't express, or (c) a third rung reinvents bank's memo+version pattern — two reinventions is a coincidence, three is an API.
Part of #115 (userver-vs-morph survey).
Observation
morph has no caching framework — every "cache" hit in
include/morph/is either an unrelated code comment or a function-localstatic const std::stringmemoizing a schema string, not a TTL/eviction/cache-aside abstraction. userver bundlescache::CacheUpdateTrait/components::CachingComponentBasewith full vs. incremental update modes and cache dumps — persisting a snapshot to survive a failed first cache load on cold start.Status: speculative, not a confirmed gap
morph has no in-memory read-cache layer today, so there's nothing to add TTL/eviction semantics to yet.
Revisit only if
morph grows an in-memory read-cache layer in front of Lightweight. If it does, the cache-dump idea (persist last-known-good state so a cold start with a broken dependency still boots) is worth considering — it rhymes with
SqliteOfflineQueue's durability goal, though it's not directly applicable without a caching framework to attach it to.Reference
Full comparison: #115, §3.7 and §4 of the attached document (
docs/superpowers/findings/userver-vs-morph-2026-08-17.md).Deep analysis: #119 caching framework
Verification. The prior investigation's findings all hold on current source, and kanban (PR #121) sharpens rather than overturns them.
bank::AccountModelremains the only hand-rolled read cache:examples/bank/include/bank/db/row_versions.hpp(a ~40-line version-counter singleton) plus a per-instance memo (_row/_loadedId/_seenVersion,examples/bank/include/bank/models/account_model.hpp) whose lock-free correctness comes from strand serialization. Onladder-kanban-impl,BoardModel::buildStatere-queries small indexed tables per call and the hot poll pathGetEventsSinceis DB-incremental (WHERE id > lastEventId). The one growing cost isGetActivity: it callsFileActionLog::entries()(full file re-read + re-parse,include/morph/journal/file_action_log.hpp:201-233), and the GUI triggers it on every applied board event (board_qml_bridge.cpp::onEventApplied) — O(journal size) per event. Kanban documents this inline as acceptable at rung scale, androtate()already provides the retention seam.Design assessment. If the trigger ever fires, the right shape is not userver's
CacheUpdateTrait(TTL/eviction/dump lifecycle) but a generalization of what bank already proved: a header-onlyVersionedCachesplit the way bank splits it — a lockedVersionRegistry<K>(writersbump(key)) plus a per-instance, lock-freeVersioned<V>memo (getOrLoad(key, registry, loader)), living ininclude/morph/util/besidedatetime.hpp/quantity.hppwith adocs/spec/util/page. TTL and eviction stay out: strand-serialized instances make explicit version stamps strictly better than time-based staleness, and morph's journal already provides natural version sources (LogEntry::seq, kanban's monotonicBoardEventRecord::id— a cached activity view could invalidate off the same cursorGetEventsSinceuses). The cache-dump idea stays inapplicable: morph clients' cold-start durability is the local SQLite file plusSqliteOfflineQueue— the authority survives restart, so there is no "last known good snapshot" distinct from the store itself. Server-side,docs/spec/core/shared_instances.mddeliberately solves the superset (cross-attacher divergence), andexamples/IMPLEMENTATION.md:138rightly forbids per-rung cache layers.Cost/benefit. Helper: S (~50-100 lines) but with real carrying cost — Doxygen-gated public API, a spec file, tests — for zero current consumers. Framework: M-L, all risk (staleness is exactly the bug class bank's pattern exists to prevent) and no beneficiary.
Recommendation: defer, unchanged, with sharpened triggers. Revisit when (a) kanban's
GetActivityshows measured cost at realistic journal sizes despiterotate(), (b) a rung needs cross-instance read state shared instances can't express, or (c) a third rung reinvents bank's memo+version pattern — two reinventions is a coincidence, three is an API.