IRIS Persistence Audit¶
Date: 2026-03-12
Scope¶
This audit covers runtime application code under backend/src and excludes Alembic migrations, where raw SQL remains an acceptable schema-management tool.
Audit method:
rgoverAsyncSession,Session,.execute(,text(,.commit(,.rollback(,.flush(.- manual review of
core/db, async-first domains, and representative sync-heavy analytical domains. - classification by persistence responsibility and migration priority.
Executive Summary¶
The repository already contains useful persistence building blocks:
backend/src/core/db/session.pycentralizes engine and session creation.backend/src/core/db/uow.pyintroduces a minimal async unit of work.backend/src/apps/control_plane/repositories.pyandbackend/src/apps/anomalies/repos/anomaly_repo.pyshow early repository patterns.
The current state is still non-uniform and violates the target standard in several ways:
- direct
AsyncSessionand syncSessionusage is widespread acrossviews,services,tasks,selectors, and domain engines. - repository boundaries are inconsistent: some domains use repositories, many still run ORM/Core queries directly inside application services or HTTP handlers.
- transaction ownership is fragmented:
commit()andflush()are called from repositories, services, selectors, engines, workers, and tasks. - read paths often return ORM entities or ad-hoc
dict[str, Any]instead of immutable typed read models. - raw SQL is concentrated in
market_datafor Timescale continuous aggregates and resampling logic, with no explicit exception policy documented yet. - loading policy is not standardized; several list/detail paths still rely on caller-side serialization from ORM entities, which keeps lazy-loading/N+1 risk alive.
- persistence logging is largely absent outside a few unrelated runtime loggers.
Quantitative Snapshot¶
Observed non-migration files with DB access or transaction ownership:
- files importing
AsyncSessionorSession: 80+ - files calling
commit(),rollback(), orflush(): 40+ - files using
text(...)or direct raw SQL execution: concentrated inmarket_data, plus a few infrastructure helpers
These counts are intentionally directional rather than contractual; the important result is the domain map below.
Domain Classification¶
Aligned or Partially Aligned¶
apps/control_plane¶
Status: migrated on the API/application surface
- Existing repositories are present in
backend/src/apps/control_plane/repositories.py. - Queries now flow through dedicated read services in
backend/src/apps/control_plane/query_services.py. - Views now depend on
get_uow()and no longer takeAsyncSessiondirectly for caller-facing reads or writes. - Read paths now return immutable dataclass models from
backend/src/apps/control_plane/read_models.py, with explicit thawing only at transport/write boundaries. - Route and draft mutation services now commit/flush via the shared UoW instead of direct session ownership.
- Structured persistence logging now covers control-plane repositories, query services and transaction lifecycle events.
- Remaining follow-up:
backend/src/apps/control_plane/cache.pystill uses its own infrastructure-local session adapter, which is acceptable for now but should eventually adopt the same logging helpers.
Classification:
OK
apps/anomalies¶
Status: migrated on the background/runtime surface
- repositories are centralized in
backend/src/apps/anomalies/repos/anomaly_repo.py - read-only anomaly list/detail flows now go through
backend/src/apps/anomalies/query_services.py - immutable dataclass read models now live in
backend/src/apps/anomalies/read_models.py - the old compatibility selector module under
backend/src/apps/anomalies/selectorshas been removed entirely; callers read anomalies directly throughbackend/src/apps/anomalies/query_services.py candle_closedconsumers and anomaly enrichment / sector-scan tasks now own persistence through the shared async UoW instead of raw session commits- persistence logging now covers anomaly repositories, query services, service orchestration and transaction lifecycle events
- sector and related-peer candle loading now batches peer reads in one explicit query path, removing the old loop-driven N+1 pattern from sector scan context building
Classification:
OK
apps/hypothesis_engine¶
Status: migrated on the API/application surface and background entrypoints
- repositories are centralized in
backend/src/apps/hypothesis_engine/repositories.py - read flows now go through
backend/src/apps/hypothesis_engine/query_services.py - views, tasks and consumers now coordinate persistence through the shared async UoW
- read paths default to immutable dataclass models from
backend/src/apps/hypothesis_engine/read_models.py - prompt loading and reasoning helpers now depend on explicit query/loader contracts instead of optional
AsyncSessionplumbing - persistence logging covers repository, query and transaction events
Classification:
OK
apps/news¶
Status: migrated on the API/application surface and background entrypoints
- repositories are isolated in
backend/src/apps/news/repositories.py - read-only list/detail flows now go through
backend/src/apps/news/query_services.py - immutable read models live in
backend/src/apps/news/read_models.py - source CRUD, polling, normalization and correlation now use the shared async UoW instead of direct session commits
- views, tasks and stream consumers no longer own raw
AsyncSessionboundaries directly - list-item reads explicitly eager-load
links, eliminating caller-side lazy loading on the public read path
Classification:
OK
Async Domains with Direct Persistence in Services¶
apps/market_structure¶
Status: migrated on the API/application surface and scheduled entrypoints
- repositories now isolate source locking, coin resolution and Core snapshot upserts in
backend/src/apps/market_structure/repositories.py - read-only plugin/source/health/snapshot/webhook flows now go through
backend/src/apps/market_structure/query_services.py - immutable dataclass read models now live in
backend/src/apps/market_structure/read_models.py - views and tasks now depend on the shared async UoW instead of owning
AsyncSessiondirectly - source CRUD, polling, manual ingest, webhook ingest and provisioning flows now commit through the shared async UoW from the caller layer instead of committing inside
backend/src/apps/market_structure/services.py - snapshot persistence stays on SQLAlchemy Core upsert, but is now isolated behind a repository and logged as an explicit bulk/Core write path
- post-commit source health, alert, delete and snapshot events now queue through generic UoW after-commit hooks, keeping side effects strictly after transaction success without leaving
commit()in the service layer
Classification:
OK
apps/market_data¶
Status: migrated on the async API/application surface and scheduled entrypoints
- repositories now isolate mutable coin/candle writes, metrics maintenance, delete cascades and Timescale aggregate refresh calls in
backend/src/apps/market_data/repositories.py - read-only coin/history/backfill candidate flows now go through
backend/src/apps/market_data/query_services.py - immutable dataclass read models now live in
backend/src/apps/market_data/read_models.py - async CRUD/history sync orchestration now lives behind class-based services in
backend/src/apps/market_data/services.py - shared candle-config and candle-event helpers now live in
backend/src/apps/market_data/support.py, so the async service/query/repository/event path no longer depends on a legacy sync DB facade - views and TaskIQ jobs now depend on the shared async UoW instead of owning
AsyncSession/AsyncSessionLocaldirectly - thin wrapper helpers formerly sitting in
backend/src/apps/market_data/views.pyandbackend/src/apps/market_data/tasks.pyhave been removed; those entrypoints now instantiatebackend/src/apps/market_data/query_services.pyplus the class-based services inbackend/src/apps/market_data/services.pydirectly, and persistence contract tests now forbid those wrappers from reappearing - query-service backfill/latest-sync selection batches latest-candle lookups, removing caller-side N+1 checks from the public async path
- pure candle/timeframe contracts now live in
backend/src/apps/market_data/candles.py, separating immutable value helpers from the async repository layer - async candle bulk reads in
backend/src/apps/market_data/repositories.pynow keep partial aggregate-view failures on a batched path and return structured partial results instead of silently degrading into per-coin fallback reads, preserving the anti-N+1 contract consumed bycross_market - sync and async candle resampling paths now fall back to in-process aggregation of base candle rows when Timescale
time_bucket/first/lastfunctions are unavailable, keeping read contracts stable on PostgreSQL-only test environments without reintroducing caller-side DB access - the old sync
backend/src/apps/market_data/service_layer.pyDB facade andbackend/src/apps/market_data/repos.pymodule have been removed; the only remaining exception surface in this domain is the documented Timescale/raw-SQL path insidebackend/src/apps/market_data/repositories.py
Classification:
OKon async/public callerskeep as justified raw SQL exceptionfor Timescale continuous aggregate refresh and dynamic aggregate-view reads/resampling in the legacy sync adapterslater migrationfor sync-heavy analytical callers still consuming the legacy sync service layer
apps/indicators¶
Status: migrated on the async API/application surface and indicator worker path
- repositories now isolate mutable indicator metrics/cache/signals/feature-snapshot writes plus feature-flag lookups and candle/aggregate reads in
backend/src/apps/indicators/repositories.py - read-only metrics/radar/flow projections now go through
backend/src/apps/indicators/query_services.py - immutable dataclass read models now live in
backend/src/apps/indicators/read_models.py - class-based async write orchestration now lives in
backend/src/apps/indicators/services.py - thin read wrappers formerly living in
market_flow.py,market_radar.pyandIndicatorReadServicehave been removed; callers and tests now usebackend/src/apps/indicators/query_services.pyandbackend/src/apps/indicators/repositories.pydirectly - views now depend on the shared async UoW instead of owning
AsyncSessiondirectly indicator_workersnow execute indicator persistence through async repositories/UoW instead ofAsyncSession.run_sync- market-radar/flow leader reads batch coin+metrics lookups, removing the old leader-path N+1 follow-up reads
- aggregate availability checks in
backend/src/apps/indicators/repositories.pyand async refresh calls inbackend/src/apps/market_data/repositories.pynow degrade to structured warning logs plus direct/resampled candle fallback or skipped refreshes when Timescale views/procedures are unavailable, keeping worker pipelines alive on PostgreSQL-only test environments - legacy sync analytical helpers were reduced to pure computation only in
backend/src/apps/indicators/analytics.py; DB access no longer lives there
Classification:
OK
apps/patterns¶
Status: migrated on the async API/application, TaskIQ orchestration and runtime worker surfaces; public sync selector surface removed and domain/ reduced to pure helper/cache modules
- repositories now isolate pattern feature and pattern registry write paths in
backend/src/apps/patterns/repositories.py - read-only pattern catalog, discovered pattern, coin regime, coin pattern, sector metrics and market-cycle projections now go through
backend/src/apps/patterns/query_services.py - immutable dataclass read models now live in
backend/src/apps/patterns/read_models.py - views now depend on the shared async UoW instead of owning
AsyncSessiondirectly - the market-cycle endpoint consumed by
indicatorsnow reuses the same query service instead of a module-level function facade - async signal projection builders now live in
backend/src/apps/patterns/query_builders.pyand are reused by bothpatternsandsignalsquery services instead of importing selector helpers from the deletedbackend/src/apps/patterns/selectors.pymodule - persistence logging now covers pattern feature/pattern registry writes and public query paths
- TaskIQ flows now run through async class-based services in
backend/src/apps/patterns/task_services.pyandbackend/src/apps/patterns/tasks.py, removing the oldAsyncSession.run_syncbridge from active runtime orchestration pattern_workersandregime_workersnow delegate incremental detection + regime refresh to async class-basedbackend/src/apps/patterns/task_service_runtime.py(PatternRealtimeService) under shared UoW ownership, removing the old syncPatternEngine/update_market_cycle/ cluster+hierarchyrun_syncpath frombackend/src/runtime/streams/workers.pydecision_workersnow delegate context enrichment plus decision/final-signal generation to asyncPatternSignalContextServiceunder UoW, removing the old syncrun_syncdecision path frombackend/src/runtime/streams/workers.pyPatternSignalContextServicenow exposesenrich_context_only(...)frombackend/src/apps/patterns/task_services.py, sosignalsruntime callers can reuse async context enrichment without invoking the broader sync compatibility flowbackend/src/apps/patterns/selectors.pyhas now been deleted entirely; callers and tests use asyncbackend/src/apps/patterns/query_services.py,backend/src/apps/patterns/services.py,backend/src/apps/signals/query_services.pyand directbackend/src/apps/patterns/query_builders.pyimports instead- async and sync
list_coin_patternsread paths now share the same explicit ordering profile frombackend/src/apps/patterns/query_builders.py, preventing timestamp-tie instability between base pattern rows and derived cluster/hierarchy rows - async regime cache clients in
backend/src/apps/patterns/cache.pyare now loop-scoped instead of process-global cached clients - async market-data candle repositories now expose range/series fetchers used by the pattern task services without pushing raw session access back into the task layer
backend/src/apps/patterns/domain/registry.py,backend/src/apps/patterns/domain/discovery.py,backend/src/apps/patterns/domain/narrative.py,backend/src/apps/patterns/domain/context.py,backend/src/apps/patterns/domain/decision.py,backend/src/apps/patterns/domain/risk.py,backend/src/apps/patterns/domain/strategy.py,backend/src/apps/patterns/domain/statistics.pyandbackend/src/apps/patterns/domain/success.pyno longer expose sync DB entrypoints; they are now pure helper/cache modules and contract tests assert the legacy persistence API stays absent- pattern-success validation is now cache-only on the runtime/bootstrap path, so active callers do not pass sync session handles through
patterns.domain.success PatternStrategyServicenow updatesStrategyRule/StrategyPerformancethrough explicit async persistence operations instead of relationship assignment that could trigger hidden lazy loads
Classification:
OK
apps/cross_market¶
Status: migrated on the async runtime/worker and test-facing surfaces; legacy sync engine removed
- repositories now isolate Core upserts for
coin_relationsandsector_metricsinbackend/src/apps/cross_market/repositories.py - read-only computation contexts now go through
backend/src/apps/cross_market/query_services.py - immutable dataclass read models now live in
backend/src/apps/cross_market/read_models.py - active worker writes now run through
backend/src/apps/cross_market/services.pyunder the shared async UoW instead ofAsyncSession.run_sync - leader/follower candle loading now batches candidate leader history through
backend/src/apps/market_data/repositories.py, removing the old loop-driven N+1 path from relation updates - correlation cache writes, prediction cache writes and emitted leader/rotation/correlation events now happen only after the persistence transaction commits on the active runtime path
backend/src/apps/cross_market/engine.pyhas now been deleted entirely; active callers and tests use asyncbackend/src/apps/cross_market/services.py,backend/src/apps/cross_market/query_services.pyandSessionUnitOfWork-backed helper harnesses insteadcross_marketleader-decision reads now flow through a typed immutable query contract inbackend/src/apps/cross_market/query_services.pyandbackend/src/apps/cross_market/read_models.pyinstead of a syncSessionhelper- async correlation cache clients in
backend/src/apps/cross_market/cache.pyare now loop-scoped likesignals/predictions, removing another shared-client edge from tests and worker runtimes
Classification:
OKon the async/background runtime and test-facing surfaces
apps/predictions¶
Status: migrated on the async API surface, scheduled evaluation job and cross-market leader path; public sync selector/wrapper surface removed, with only internal sync engine impls left for residual compatibility/test callers
- repositories now isolate prediction candidate selection, pending-window checks and explicit relation-feedback locks in
backend/src/apps/predictions/repositories.py - read-only prediction list/detail flows now go through
backend/src/apps/predictions/query_services.py - immutable dataclass read models now live in
backend/src/apps/predictions/read_models.py - API reads now depend on the shared async UoW instead of injecting
AsyncSessiondirectly inbackend/src/apps/predictions/views.py - scheduled evaluation now runs through
backend/src/apps/predictions/services.pyunder the shared async UoW, with cache writes and published events deferred until after commit - cross-market leader detection now calls the same class-based prediction service instead of issuing direct prediction writes through a module-level async helper
- creation now batches pending-window lookups by leader/target set, removing the old per-relation pending-check N+1 path from the active async flow
- shared prediction constants/outcome helpers used by async services now live in
backend/src/apps/predictions/support.py - the old
backend/src/apps/predictions/engine.pyalias has been removed entirely; callers depend onbackend/src/apps/predictions/services.pydirectly - async prediction cache clients in
backend/src/apps/predictions/cache.pyare now loop-scoped instead of process-global cached objects - the sync
backend/src/apps/predictions/selectors.pylayer has now been removed; active callers and tests read predictions only through asyncbackend/src/apps/predictions/query_services.py -
low-level tests now execute prediction creation/evaluation through
SessionUnitOfWork-backed service harnesses instead of internal sync engine helpers Classification: -
OK
apps/signals¶
Status: migrated on the async/public API read surface plus signal-fusion and signal-history runtime surfaces; legacy sync module entrypoints removed
- read-only signal, decision, market-decision, final-signal, backtest and strategy projections now go through
backend/src/apps/signals/query_services.py - immutable dataclass read models now live in
backend/src/apps/signals/read_models.py - views now depend on the shared async UoW instead of injecting
AsyncSessiondirectly inbackend/src/apps/signals/views.py - write-side signal-fusion persistence now goes through
backend/src/apps/signals/repositories.py - history writes now also go through
backend/src/apps/signals/repositories.py backend/src/apps/signals/services.pynow hosts only class-based asyncSignalFusionService,SignalHistoryServiceand post-commit side-effect dispatchers on the active runtime path, without re-exporting legacy sync compatibility helpersbackend/src/runtime/streams/workers.pynow routessignal_fusion_workersthrough the shared async UoW instead of opening sync write boundaries insidefusion.pybackend/src/runtime/streams/workers.pynow refreshes signal history through the shared async UoW instead of callingrefresh_recent_signal_history()inside the sync decision flowbackend/src/apps/patterns/task_service_history.pynow delegates signal-history refresh toSignalHistoryService, removing the duplicated async history persistence path- active async query/service code now uses
backend/src/apps/signals/backtest_support.py,backend/src/apps/signals/fusion_support.pyandbackend/src/apps/signals/history_support.pyinstead of importing pure helper logic from the legacy sync compatibility modules directly - low-level tests now exercise recent-signal selection and history signal fetching through async
backend/src/apps/signals/services.py/backend/src/apps/signals/repositories.pyinstead of the old sync query helpers that used to live infusion.py/history.py - active async query paths now resolve latest decision/final-signal/market-decision ranking subqueries via
backend/src/apps/signals/query_builders.py, removing direct imports from compatibility selector modules insidebackend/src/apps/signals/query_services.py - the public sync read modules formerly living in
backend/src/apps/signals/backtests.py,backend/src/apps/signals/strategies.py,backend/src/apps/signals/decision_selectors.py,backend/src/apps/signals/market_decision_selectors.pyandbackend/src/apps/signals/final_signal_selectors.pyhave now been removed; callers and tests read only throughbackend/src/apps/signals/query_services.py, while reusable helper logic stays in support modules such asbackend/src/apps/signals/backtest_support.py backend/src/apps/signals/fusion.pyandbackend/src/apps/signals/history.pyhave now been deleted entirely; callers and tests write only throughbackend/src/apps/signals/services.py, while deterministic helper logic lives directly inbackend/src/apps/signals/fusion_support.pyandbackend/src/apps/signals/history_support.pySignalFusionServicenow enriches pattern context through asyncbackend/src/apps/patterns/task_services.py(PatternSignalContextService.enrich_context_only) under shared UoW ownership, removing the oldAsyncSession.run_syncbridge topatterns.domain.contextbackend/tests/apps/patterns/test_evaluation_job.pynow exercises asyncbackend/src/apps/patterns/task_services.pyPatternEvaluationService, and the legacy sync orchestration helperbackend/src/apps/patterns/domain/evaluation.pyhas been removed- market-decision detail reads keep their cache-first behavior but the fallback and DB projection are now logged through the shared persistence logger inside
SignalQueryService
Classification:
OKon the async/public API, runtime worker and test-facing read/write surfaces
apps/portfolio¶
Status: migrated on the async/public API, scheduled balance-sync, runtime worker and test-facing helper surfaces; legacy sync selector/engine entrypoints removed
- read-only portfolio projections now go through
backend/src/apps/portfolio/query_services.py - immutable dataclass read models now live in
backend/src/apps/portfolio/read_models.py - write-side balance/account/state persistence now goes through
backend/src/apps/portfolio/repositories.py /portfolio/*views now depend on the shared async UoW instead of injectingAsyncSessiondirectly inbackend/src/apps/portfolio/views.pyportfolio_sync_jobnow runs throughbackend/src/apps/portfolio/services.pyunder the shared async UoW, with cache writes and published events deferred until after commitportfolio_workersnow evaluate portfolio actions through the class-based asyncPortfolioServiceunder the shared async UoW, with event/cache side effects applied post-commit- async portfolio decision-ranking projection now uses
backend/src/apps/portfolio/query_builders.py, and shared position-sizing/stop helpers now live inbackend/src/apps/portfolio/support.pyso neither services nor tests depend onportfolio.engine - async portfolio cache clients in
backend/src/apps/portfolio/cache.pyare now loop-scoped instead of process-global cached clients - the active balance-sync path no longer re-fetches
ExchangeAccountper balance row, removing an avoidable per-item read on the loop - consumer tests in
backend/tests/apps/portfolio/test_sync_worker.py,backend/tests/apps/portfolio/test_auto_watch_feature.py,backend/tests/apps/portfolio/test_risk_management.py,backend/tests/apps/portfolio/test_engine.pyandbackend/tests/apps/portfolio/test_engine_branches.pynow exercise asyncbackend/src/apps/portfolio/services.pyplusPortfolioSideEffectDispatcherfor state, helper and balance-sync coverage - the public sync selector API formerly living in
backend/src/apps/portfolio/selectors.pyhas been removed entirely; callers and tests read state/actions/positions only through asyncbackend/src/apps/portfolio/query_services.py - the public sync
portfolio.enginewrappers forensure_portfolio_state,refresh_portfolio_state,evaluate_portfolio_actionandsync_exchange_balancesare gone, andbackend/src/apps/portfolio/engine.pyhas now been deleted instead of kept as an empty tombstone
Classification:
OKon the async/public API, scheduled sync, runtime worker and test-facing surfaces
Sync-Heavy Analytical Domains¶
No application domains are still dominated by synchronous Session access inside selector/engine compatibility modules.
Classification:
move to repositorymove to query servicereplace ORM leakage with typed modelfix transaction boundaryfix N+1/loading contractadd logging
Priority note:
- these domains should migrate after the async-first domains because they require both boundary cleanup and sync-to-async strategy decisions.
Cross-Cutting Findings¶
Direct DB Access from API Surface¶
Direct session injection on migrated FastAPI surfaces has been removed. Remaining DB-bound caller drift is concentrated in legacy sync analytical helpers rather than views.py.
Runtime note:
analysis_scheduler_workersnow useAnalysisSchedulerServiceunder shared async UoW ownership instead of directAsyncSessionreads/commits inbackend/src/runtime/streams/workers.py.
Async Test Persistence Drift¶
Recent cleanup:
- async worker/pipeline regression tests in
anomalies,signals,cross_marketandportfoliono longer open ad-hoc syncSessionLocal()handles insidepytest.mark.asyncioflows - those tests now verify committed worker results through shared
db_session/async_db_sessionfixtures plus explicit Redis stream/event waits, keeping test persistence boundaries aligned with the runtime model patterns.domain.regimeandpatterns.domain.schedulerno longer expose sync DB helper functions; async/query caller paths now go throughPatternQueryService.compute_live_regimes(...)andAnalysisSchedulerService.evaluate_indicator_update(...)patterns.domain.enginehas been deleted; active detection/bootstrap coverage now goes throughPatternRealtimeServiceandPatternBootstrapService, and contract tests assert the sync engine module is absentpatterns.domain.clustersandpatterns.domain.hierarchyhave been deleted; meta-signal coverage now runs only throughPatternRealtimeService, and contract tests assert both sync modules are absentpatterns.domain.cycleno longer exposes sync DB mutation entrypoints; cycle updates now run only throughPatternRealtimeService._update_market_cycle(...)andPatternMarketStructureService.refresh()- remaining
patterns.domainmodules no longer expose sync DB entrypoints either; tests now seed catalog metadata throughtests.patterns_support.seed_pattern_catalog_metadata(...)and exercise async task/query services instead of the removed sync domain API market_data.reposhas been deleted; candle/timeframe value contracts now live insrc.apps.market_data.candles, sync test seeding moved underbackend/tests/market_data_support.py, and contract tests assert the legacy sync module stays absent
Transaction Boundary Drift¶
Representative offenders: - no active async/public or runtime service-owned commit offenders remain after the final caller-owned transaction cleanup.
Recently fixed:
- analysis scheduler stream handling in
backend/src/runtime/streams/workers.pyno longer commits through direct session ownership. - write-side helper functions in
backend/src/apps/market_data/services.pynow require a shared async UoW boundary instead of accepting bareAsyncSession/ mixed write contracts. backend/src/apps/market_structure/services.pyno longer ownscommit()either; write callers now commit explicitly and side effects are queued viaBaseAsyncUnitOfWork.add_after_commit_action(...).- the remaining write services in
news,hypothesis_engine,anomalies,cross_market,patterns,market_dataandcontrol_planeno longer commit internally; their HTTP routes, tasks and runtime workers now close the transaction explicitly. market_datahistory sync helper paths now flush only, while aggregate refreshes, candle events and analysis/history notifications are queued as post-commit actions on the shared async UoW.
Required action:
- repositories may
flush()when required for generated IDs or lock sequencing. - application services / workers / tasks must own
commit()androllback(). - query services must never commit.
ORM Leakage and Untyped Read Contracts¶
Representative offenders:
market_datastill serializes ORM-backed state inside service/view logic.- selectors in
patterns,signals, andportfolioreturndict[str, Any].
Required action:
- default read contract becomes immutable dataclass read models.
- mutable ORM access must be explicit via write-side repository methods such as
get_for_update(...).
Raw SQL Status¶
Current raw SQL outside migrations is concentrated in:
backend/src/apps/market_data/repositories.py
Assessment:
- dynamic continuous aggregate view reads and
refresh_continuous_aggregateare acceptable documented exceptions because they are vendor-specific Timescale behavior. - raw SQL should not spread beyond this infrastructure boundary.
Logging Gap¶
Shared persistence logging now exists under core/db and is exercised in migrated domains. Remaining gaps are concentrated in unmigrated domains, where repository/query abstractions are still absent and DB access therefore bypasses the structured logger.
Migration Order¶
Recommended rollout order:
- completed: shared persistence foundation in
core/db - completed:
apps/hypothesis_engine - completed:
apps/control_plane - completed:
apps/news - completed:
apps/market_structure - completed:
apps/anomalies - completed:
apps/market_data - completed:
apps/indicators - completed on the async/public, TaskIQ orchestration and runtime worker surfaces:
apps/patterns - completed on the async/background runtime and test-facing surfaces:
apps/cross_market - completed on the async/public API and scheduled runtime surface:
apps/predictions - completed on the async/public API plus signal-fusion/signal-history runtime and test-facing surfaces:
apps/signals - completed on the async/public API and scheduled sync surface:
apps/portfolio - completed: remove residual sync DB helpers from
apps/patterns/domain/* - optional later: re-evaluate vendor-specific raw SQL exceptions in
market_data/indicatorsif Timescale-specific Core abstractions become worthwhile - optional later: final doc cleanup once temporary audit notes move from
docs/_tmpinto permanent architecture docs
Current Behavior To Preserve¶
- all existing HTTP routes and payload shapes must remain backward-compatible during migration.
- background workers and scheduled jobs must continue to use the same event types and Redis side effects.
- Timescale aggregate refresh behavior in
market_datamust remain semantically identical. - control-plane topology publish/draft behavior must remain unchanged.
- hypothesis prompt caching and invalidation semantics must remain unchanged.
- news and market-structure source provisioning flows must remain unchanged for the frontend.