Shop It Docs
Portfolio Module

Caching

Redis cache keys, TTLs, and invalidation scopes for portfolio reads

Two read endpoints are Redis-backed; everything else hits Postgres directly. Cache invalidation is structurally enforced via shared.TxRunner.Run — every write declares a CacheScope at the call site, and the post-commit Invalidator.Apply busts exactly the right keys.

Cached endpoints

EndpointKey patternTTLInvalidator scope
GET /portfolios/{id}portfolio:detail:{id}30sDetail
GET /portfolios/{id}/summary(same — FindSummary delegates to FindDetail)30sDetail
GET /portfolios/{id}/valuation?range=Rportfolio:valuation:{id}:{R}60s for 1D, 5min for daily rangesValuation (busts all 8 ranges)

R{1D, 1W, 1M, 3M, 6M, 1Y, YTD, ALL}. There are 8 separate cache keys per portfolio for valuation — one per range — and they are always invalidated as a group.

Cache scopes

Defined in internal/modules/portfolio/shared/cache.go:

type CacheScope int

const (
    CacheScopeNone CacheScope = iota          // no bust — for reads
    CacheScopeDetail                          // bust portfolio:detail:{id}
    CacheScopeDetailAndValuation              // bust detail + all valuation ranges
)

Which scope each write uses

PathScopeReason
CreateNoneNew portfolio has no cache yet
Update (name, description, isDefault)DetailHeader changes affect bundled detail
DeleteDetailAndValuationAll derived numbers go to zero
Transaction Add / Update / DeleteDetailAndValuationSource data changed; all derivations stale

Pagination snapshot

FindAll (portfolios) and Find (transactions) wrap their rows-query and count-query in one read-only REPEATABLE READ transaction via TxRunner.RunReadSnapshot. A concurrent insert/delete between the two queries would otherwise yield an inconsistent (rows, count) pair. No Redis caching here — Postgres consistency is the guarantee.

Cache miss path

The FindDetail flow short-circuits on cache hit:

if found := cache.GetJSON(detailKey, &snapshot); found {
    return snapshotToDetail(snapshot)
}
// otherwise: read transactions + prices, BuildState, cache.SetJSON, return

Cache misses never block writes — slog.Warn on cache read/write failure and fall through to Postgres.