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
| Endpoint | Key pattern | TTL | Invalidator scope |
|---|---|---|---|
GET /portfolios/{id} | portfolio:detail:{id} | 30s | Detail |
GET /portfolios/{id}/summary | (same — FindSummary delegates to FindDetail) | 30s | Detail |
GET /portfolios/{id}/valuation?range=R | portfolio:valuation:{id}:{R} | 60s for 1D, 5min for daily ranges | Valuation (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
| Path | Scope | Reason |
|---|---|---|
| Create | None | New portfolio has no cache yet |
| Update (name, description, isDefault) | Detail | Header changes affect bundled detail |
| Delete | DetailAndValuation | All derived numbers go to zero |
| Transaction Add / Update / Delete | DetailAndValuation | Source 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, returnCache misses never block writes — slog.Warn on cache read/write failure and fall through to Postgres.