Skip to content

refactor(storage): split Storage into per-aggregate stores#448

Draft
0x46616c6b wants to merge 3 commits into
mainfrom
refactor/split-storage-interface
Draft

refactor(storage): split Storage into per-aggregate stores#448
0x46616c6b wants to merge 3 commits into
mainfrom
refactor/split-storage-interface

Conversation

@0x46616c6b

Copy link
Copy Markdown
Member

Summary

The storage.Storage interface had grown to 44 methods spanning users, tickers, ticker↔user membership, ticker websites, the four integration configs, uploads, messages, three settings pairs, and a filesystem path. There was a single production implementation (SqlStorage) and a 2,631-line generated mock that every handler imported in full. Splitting along aggregates gives each handler a narrow dependency, shrinks the mocking surface per call site, and gives us clear seams for follow-up work (e.g. a future message-publication module).

What changed

  • Five focused stores under internal/storage/: UserStore, TickerStore, MessageStore, UploadStore, SettingsStore. Each carries WithXxxTx(*gorm.DB) for transactional scoping.
  • Integration enum replaces the four DeleteTelegram / DeleteMastodon / DeleteBluesky / DeleteSignalGroup methods with a single ClearIntegration(ticker, Integration); DeleteIntegrations becomes ClearIntegrations. Naming stays Integration to match the existing domain term.
  • Generic settings: GetSettings[T] / SaveSettings[T] over a tiny GetSetting/SaveSetting interface, with InactiveSetting, TelegramSetting, SignalGroupSetting descriptors.
  • Stores aggregate carried by handlers, the bridge layer, and cmd/; UnitOfWork.Do(fn func(Tx) error) scopes all five stores to one GORM transaction.
  • Bridges narrowed to only the stores they need (uploads, settings), removing the full storage.Storage dependency.
  • Prefetch middlewares take their relevant narrow store directly (e.g. PrefetchTicker(TickerStore)).
  • Test infra: a hand-rolled MockStorage wrapper (77 LOC) bundles the five mockery-generated per-store mocks, exposing a Stores() accessor for handler tests. Settings helpers (MockGetTelegram, MockSaveSignalGroup, …) preserve the previous test ergonomics.
  • The old Storage interface and mock_Storage.go are deleted.

Why

  • Locality: each handler's storage dependencies are now visible at the call site instead of hidden behind a 44-method interface.
  • Smaller mock surface: per-aggregate mocks scale with the size of each aggregate, not the whole system. The 2.6 KLOC monolithic mock is replaced by focused mocks per store (mockery generates them automatically from .mockery.yml).
  • Unblocks downstream refactors — a future "publish a message" orchestrator (currently fire-and-forget bridge fan-out in handlers) gets a clean shape to depend on.

The changes and the PR were generated by Claude.

0x46616c6b and others added 3 commits May 27, 2026 16:47
The 44-method `storage.Storage` interface was a single shape shared by
every handler, middleware, and bridge — its generated mock alone was
2.6 KLOC and grew with every method. Replace it with five focused
stores carried by a `Stores` aggregate: UserStore, TickerStore (owning
membership, websites, and the four integration configs collapsed to
ClearIntegration/ClearIntegrations), MessageStore, UploadStore, and a
SettingsStore with a generic GetSettings[T]/SaveSettings[T] surface
over the three settings rows. Each store also exposes a WithXxxTx
method, and a new UnitOfWork.Do scopes them to a shared transaction.

Bridges now hold only the stores they need (UploadStore + SettingsStore
for Telegram and Signal; UploadStore for Mastodon and Bluesky), and
the prefetch middlewares take their narrow store directly.

Co-Authored-By: Claude <claude@anthropic.com>
Drive-by — gofmt flagged the missing space after `//` on the comment.

Co-Authored-By: Claude <claude@anthropic.com>
The UnitOfWork interface, Tx struct, and per-store WithXxxTx helpers
introduced alongside the storage interface split had no production
callers and no tests exercising them. Drop the dead infrastructure
until a real consumer exists.

- Delete unit_of_work.go (UnitOfWork, Tx, Do)
- Delete mock_UnitOfWork.go
- Remove WithXxxTx from all five store interfaces, impls, and mocks
- Drop MockStorage.UoW field and the unused MockGetSettingMissing helper
- Keep Stores + NewStores in new stores.go

Co-Authored-By: OpenCode <noreply@opencode.ai>
@sonarqubecloud

sonarqubecloud Bot commented Jun 2, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant