Problem
WalletCache._key (app/services/wallet_cache.py):
def _key(self, address: str) -> str:
return f"wallet:{address}"
WalletRepository (app/repositories/wallet_repository.py) provides both get_by_user_id and get_by_public_key, and the wallet model has two stable identities (user_id unique, public_key unique). The cache only indexes by address, so:
- A user-id lookup can never hit the cache (the gap tracked separately), and
- The address-keyed entry is written from user-id reads (
_fetch_wallet does cache.set(wallet.public_key, ...)), meaning the cache stores the same wallet object under the address key even though that entry is only consulted by address lookups.
Consequences:
- Cache entries and DB identity can diverge: if a wallet is re-linked (same user, new address —
link_wallet allows this), the old address key retains the stale wallet indefinitely until TTL, while the DB has the new mapping; an address-based read (get_balance) can return the pre-relink wallet.
- The namespace conflates two lookup directions: there is no way to invalidate "all keys for user X" (no
invalidate_prefix usage from WalletRegistry), so relink/update paths cannot clear the affected entries.
TTLCache.invalidate_prefix exists in app/utils/cache.py as the house pattern for this, but WalletCache has no prefix-invalidation at all.
Root cause
The cache was designed around the address lookup only, and the write/read paths never reconciled the two-key identity model.
Why this is architecturally hard
- The durable fix is either two namespaces (
wallet:{address}, wallet:user:{user_id}) with invalidation on every write path in WalletRepository/WalletRegistry, or a single canonical key with lookup-by-index — a data-model decision with cache-coherence implications.
- Relink semantics (
link_wallet changing the address for a user) require invalidating both the old and new address keys plus the user key atomically; Redis multi/pipe is the mechanism, and the ordering with the DB commit matters.
- Tests must cover the relink scenario (old address stale, new address fresh, user lookup fresh) — none exist today.
Proposed design
Add a user-id key namespace, invalidate both namespaces on every create/link/update, implement WalletCache.invalidate_prefix mirroring TTLCache, and add relink tests asserting no stale reads.
Acceptance criteria
Service
Tests
Out of scope
The user-id cache gap (tracked separately) and simulated balances (tracked separately).
Getting started
pytest tests/test_wallet_persistence.py -q
make typecheck
Good first files to read: app/services/wallet_cache.py, app/services/wallet_registry.py, app/repositories/wallet_repository.py.
Problem
WalletCache._key(app/services/wallet_cache.py):WalletRepository(app/repositories/wallet_repository.py) provides bothget_by_user_idandget_by_public_key, and the wallet model has two stable identities (user_idunique,public_keyunique). The cache only indexes by address, so:_fetch_walletdoescache.set(wallet.public_key, ...)), meaning the cache stores the same wallet object under the address key even though that entry is only consulted by address lookups.Consequences:
link_walletallows this), the old address key retains the stale wallet indefinitely until TTL, while the DB has the new mapping; an address-based read (get_balance) can return the pre-relink wallet.invalidate_prefixusage fromWalletRegistry), so relink/update paths cannot clear the affected entries.TTLCache.invalidate_prefixexists inapp/utils/cache.pyas the house pattern for this, butWalletCachehas no prefix-invalidation at all.Root cause
The cache was designed around the address lookup only, and the write/read paths never reconciled the two-key identity model.
Why this is architecturally hard
wallet:{address},wallet:user:{user_id}) with invalidation on every write path inWalletRepository/WalletRegistry, or a single canonical key with lookup-by-index — a data-model decision with cache-coherence implications.link_walletchanging the address for a user) require invalidating both the old and new address keys plus the user key atomically; Redis multi/pipe is the mechanism, and the ordering with the DB commit matters.Proposed design
Add a user-id key namespace, invalidate both namespaces on every create/link/update, implement
WalletCache.invalidate_prefixmirroringTTLCache, and add relink tests asserting no stale reads.Acceptance criteria
Service
Tests
Out of scope
The user-id cache gap (tracked separately) and simulated balances (tracked separately).
Getting started
Good first files to read:
app/services/wallet_cache.py,app/services/wallet_registry.py,app/repositories/wallet_repository.py.