# Cross-Service Repository Architecture Matrix ## Quick Reference Table | Feature | Trading Service | Backtesting Service | ML Training Service | API Gateway | |---------|---|---|---|---| | **Uses Repositories** | Yes (4) | Yes (3) | Yes (1) | No | | **Trait Count** | 4 | 4 | 1 | 0 | | **Mock Pattern** | Inline #[cfg(test)] | Dedicated structs | Stateful RwLock | N/A | | **Factory Method** | No | Yes | No | N/A | | **Error Type** | Custom enum | anyhow | anyhow | N/A | | **Composition** | Direct SQLx | Provider wrapper | Delegation | N/A | | **DI Pattern** | Constructor | Factory trait | Constructor | N/A | | **Test Support** | Fair | Excellent | Excellent | N/A | | **Production Ready** | Yes | Yes | Yes | Yes | | **Reference Impl** | No | **YES** | No | N/A | ## Code Metrics | Metric | Trading | Backtesting | ML Training | Total | |--------|---------|-------------|-------------|-------| | Trait LOC | 319 | 301 | 63 | 683 | | Impl LOC | 1,448 | 365 | 230 | 2,043 | | Mock LOC | Varies | 112 | 102 | 214+ | | Total LOC | 1,767 | 666+ | 293 | 2,726+ | | Impl Count | 4 | 4 | 1 | 9 | | Traits/Impl | 1:1 | 1:1 | 1:1 | 3:9 | ## Decision Tree: Which Service to Model After? ``` Do you need multiple repositories? ├─ Yes → Use Backtesting Service pattern │ (factory, combined trait, dedicated mocks) │ └─ No → Use ML Training Service pattern (single repo, direct injection, stateful mock) Do you need environment-based selection? ├─ Yes → Use Backtesting Service pattern with create_repositories() │ └─ No → Either pattern is fine Do you need real data + mocks at runtime? ├─ Yes → Use Backtesting Service pattern │ (supports both via factory) │ └─ No → Either pattern is acceptable Do you need production-like test behavior? ├─ Yes → Use ML Training pattern │ (stateful RwLock mocks) │ └─ No → Use Backtesting pattern (simple empty-return mocks) ``` ## Repository Trait Checklist When creating a new repository trait, ensure: - [ ] `#[async_trait]` macro applied - [ ] `Send + Sync` bounds included - [ ] All methods are `async` - [ ] Return type is `Result` (anyhow or custom) - [ ] Methods are fine-grained (single responsibility) - [ ] No database-specific concerns leak into trait - [ ] Trait is `pub` for use across crates - [ ] Documented with examples Example: ```rust /// Repository trait for domain-specific operations #[async_trait] pub trait MyRepository: Send + Sync { /// Descriptive method documentation async fn operation(&self, param: Type) -> Result; } ``` ## Implementation Patterns Comparison ### Pattern A: Direct Database (Trading Service) ``` Business Logic ↓ PostgresTradingRepository (impl TradingRepository trait) ↓ SQLx (direct database queries) ↓ PostgreSQL ``` - Pros: Simple, straightforward - Cons: Tight coupling to database - Use: When you control the entire data access layer ### Pattern B: Provider Wrapper (Backtesting Service) ⭐ RECOMMENDED ``` Business Logic ↓ StorageManagerTradingRepository (impl TradingRepository trait) ↓ StorageManager (business logic) ↓ Database/Provider (flexible) ``` - Pros: Loose coupling, flexible, testable, factory-friendly - Cons: Extra abstraction layer - Use: New services, multiple data sources, complex logic ### Pattern C: Delegation (ML Training Service) ``` Business Logic ↓ PostgresMlDataRepository (impl MlDataRepository trait) ↓ DatabaseManager (delegates all calls) ↓ SQLx (direct queries via DatabaseManager) ``` - Pros: Simple, but still abstracted - Cons: Minimal value add over direct access - Use: When DatabaseManager already exists ## Error Handling Comparison ### Trading Service (Custom) ```rust pub type TradingServiceResult = Result; pub enum TradingServiceError { ValidationError { message: String }, TimestampConversion { timestamp: i64 }, NetworkError { details: String }, // ... } // Usage async fn store_order(&self, order: &TradingOrder) -> TradingServiceResult { Ok(id) } ``` - Pros: Type-safe, custom error handling - Cons: Diverges from codebase standard - Status: **NEEDS MIGRATION** to anyhow ### Backtesting/ML Training (anyhow) ```rust pub type Result = anyhow::Result; // Usage async fn create_training_job(&self, job: &TrainingJobRecord) -> Result<()> { Ok(()) } ``` - Pros: Standardized, composable, ergonomic - Cons: Less type-safe error handling - Status: **STANDARD** for new code ## Dependency Injection Comparison ### Direct Constructor Injection ```rust pub struct PostgresMlDataRepository { database: Arc, } impl PostgresMlDataRepository { pub fn new(database: Arc) -> Self { Self { database } } } // Usage let repo = Arc::new(PostgresMlDataRepository::new(db_manager)); ``` ### Factory Function with Environment Selection ```rust pub async fn create_repositories(storage: Arc) -> Result { let use_dbn = std::env::var("USE_DBN_DATA") .ok() .and_then(|v| v.parse::().ok()) .unwrap_or(false); let market_data: Box = if use_dbn { Box::new(DbnMarketDataRepository::new_with_mappings(mappings).await?) } else { Box::new(DataProviderMarketDataRepository::new().await?) }; Ok(DefaultRepositories { market_data, ... }) } // Usage let repos = create_repositories(storage).await?; ``` **Recommendation**: Use factory for services with multiple implementations or env-based selection ## Mock Strategy Comparison ### Approach A: Inline Mocks (Trading) ```rust #[cfg(test)] mod tests { // Mocks defined inline when needed // Only available during testing } ``` - Isolation: High - Reusability: Low - Boilerplate: Minimal ### Approach B: Dedicated Mock Structs (Backtesting) ⭐ RECOMMENDED ```rust pub struct MockMarketDataRepository; #[async_trait] impl MarketDataRepository for MockMarketDataRepository { async fn load_historical_data(...) -> Result> { Ok(vec![]) } } ``` - Isolation: Low - Reusability: High - Boilerplate: Moderate - **Advantage**: Can be used in production if needed (feature flag) ### Approach C: Stateful In-Memory (ML Training) ```rust #[cfg(test)] pub struct MockMlDataRepository { jobs: Arc>>, } #[cfg(test)] #[async_trait] impl MlDataRepository for MockMlDataRepository { async fn create_training_job(&self, job: &TrainingJobRecord) -> Result<()> { let mut jobs = self.jobs.write().await; jobs.insert(job.id, job.clone()); Ok(()) } } ``` - Isolation: Medium - Reusability: Medium - Boilerplate: High - **Advantage**: Realistic test behavior, state verification **Best Practice**: Use **Approach B** for new services ## Migration Path for Trading Service To align Trading Service with Backtesting/ML patterns: ### Step 1: Error Type Migration ```rust // Before pub type TradingServiceResult = Result; // After pub type Result = anyhow::Result; ``` ### Step 2: Mock Pattern Migration ```rust // Before: Inline in tests // After: Dedicated mock structs pub struct MockTradingRepository; #[async_trait] impl TradingRepository for MockTradingRepository { ... } ``` ### Step 3: Consider Factory Pattern ```rust // If multiple repository implementations exist, // add factory function for DI: pub async fn create_repositories(pool: PgPool) -> Result { let trading = Box::new(PostgresTradingRepository::new(pool)); let market_data = Box::new(PostgresMarketDataRepository::new(pool)); // ... } ``` **Effort**: 2-3 hours, low risk, high value ## Testing Strategy by Service ### Trading Service ``` Unit Tests (per repository impl) ↓ Integration Tests (services + repos) ↓ E2E Tests (full stack) Mock Support: Inline mocks (added per test) ``` ### Backtesting Service (RECOMMENDED APPROACH) ``` Unit Tests (mock repositories with empty returns) ↓ Integration Tests (real repositories + providers) ↓ E2E Tests (end-to-end backtesting runs) Mock Support: Dedicated mock structs + factory selection ``` ### ML Training Service ``` Unit Tests (stateful in-memory mock repositories) ↓ Integration Tests (real postgres repository) ↓ E2E Tests (full training pipeline) Mock Support: Full in-memory HashMap-backed mock ``` ## API Gateway (Why No Repositories) API Gateway correctly **does NOT use repositories** because: 1. **Network Boundary**: Gateway is the entry point, not data layer 2. **Stateless Design**: Should not hold business state 3. **Proxy Pattern**: Routes to downstream services 4. **Separation of Concerns**: - ✓ Authentication, authorization - ✓ Rate limiting, audit logging - ✓ Request routing, health checks - ✗ Data persistence, business logic Example correct structure: ```rust // API Gateway pub struct ApiGateway { trading_client: TradingServiceClient, backtesting_client: BacktestingServiceClient, ml_training_client: MlTrainingServiceClient, auth: AuthLayer, rate_limiter: RateLimiter, } // NO repositories here // Each downstream service manages its own data ``` --- ## Summary: Quick Decision Guide **For new services:** 1. Choose Backtesting Service as template 2. Use dedicated mock structs 3. Use factory pattern for DI if multiple repos exist 4. Standardize on anyhow::Result 5. Add factory function for environment-based selection **For existing services:** 1. Keep current implementation (already working) 2. Document patterns in REPOSITORY_PATTERN.md 3. Plan gradual migration to backtesting patterns 4. Start with error type standardization (low risk) **For API Gateway:** - Keep as-is (correct by design, no repositories)