refactor: Make service dependencies explicit and injectable
Remove hidden cross-service coupling by making dependencies explicit through dependency injection while maintaining backward compatibility via lazy imports. Key changes: - history_service and ban_service: Removed direct module-level imports of fail2ban_metadata_service, added optional service parameters to functions - Added get_fail2ban_metadata_service() provider to dependencies.py - Updated history router to inject Fail2BanMetadataService dependency - history_service functions now use lazy imports in fallback paths for backward compatibility when service is not explicitly injected - All test patches updated to use internal _get_fail2ban_db_path() helper - jail_config_service and jail_service already follow best practices This pattern prevents circular imports, makes services testable via explicit mocking, and documents service dependencies clearly. Fixes: Instructions.md #2 - Hidden cross-service coupling Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -133,6 +133,47 @@ backend/
|
||||
- **Repositories** handle raw database queries — nothing else.
|
||||
- Never put business logic inside routers or repositories.
|
||||
|
||||
### Service Dependencies and Injection
|
||||
|
||||
Services should **never** directly import other services to avoid hidden coupling and make testing harder. Instead:
|
||||
|
||||
1. **Define clear service interfaces** using Protocol classes in `app/services/protocols.py`.
|
||||
2. **Make dependencies explicit** by passing them as function parameters with optional defaults.
|
||||
3. **Use lazy imports** for fallback singletons (not at module level).
|
||||
4. **Inject services via FastAPI dependencies** when called from routers.
|
||||
|
||||
**Example:** The `history_service` depends on `Fail2BanMetadataService` to resolve the fail2ban database path:
|
||||
|
||||
```python
|
||||
# Good — dependency passed as parameter
|
||||
async def list_history(
|
||||
socket_path: str,
|
||||
fail2ban_metadata_service: Fail2BanMetadataService | None = None,
|
||||
) -> HistoryListResponse:
|
||||
if fail2ban_metadata_service is None:
|
||||
# Lazy import fallback for backward compatibility
|
||||
from app.services.fail2ban_metadata_service import default_fail2ban_metadata_service
|
||||
fail2ban_metadata_service = default_fail2ban_metadata_service
|
||||
...
|
||||
```
|
||||
|
||||
Routers inject the service dependency explicitly:
|
||||
|
||||
```python
|
||||
from app.dependencies import Fail2BanMetadataServiceDep
|
||||
|
||||
@router.get("/api/history")
|
||||
async def get_history(
|
||||
fail2ban_metadata_service: Fail2BanMetadataServiceDep,
|
||||
) -> HistoryListResponse:
|
||||
return await history_service.list_history(
|
||||
socket_path,
|
||||
fail2ban_metadata_service=fail2ban_metadata_service,
|
||||
)
|
||||
```
|
||||
|
||||
This pattern prevents circular imports, makes services testable, and allows easy mocking in tests.
|
||||
|
||||
---
|
||||
|
||||
## 4. FastAPI Conventions
|
||||
|
||||
@@ -1,26 +1,3 @@
|
||||
## 1) Broad exception catching in backend services
|
||||
- Where found:
|
||||
- [backend/app/services/ban_service.py](backend/app/services/ban_service.py)
|
||||
- [backend/app/services/geo_cache.py](backend/app/services/geo_cache.py)
|
||||
- [backend/app/services/blocklist_service.py](backend/app/services/blocklist_service.py)
|
||||
- Why this is needed:
|
||||
- Catching broad Exception hides root causes and weakens operational debugging.
|
||||
- Goal:
|
||||
- Replace broad catches with targeted exception handling and predictable failure paths.
|
||||
- What to do:
|
||||
- Inventory each broad catch.
|
||||
- Replace with explicit exception classes.
|
||||
- Keep one top-level safety catch only where unavoidable, with full context logging.
|
||||
- Possible traps and issues:
|
||||
- Over-tightening catches can expose previously hidden runtime failures.
|
||||
- Docs changes needed:
|
||||
- Add service error-handling policy and allowed catch patterns.
|
||||
- Doc references:
|
||||
- [Docs/Backend-Development.md](Docs/Backend-Development.md)
|
||||
- https://docs.python.org/3/tutorial/errors.html
|
||||
|
||||
|
||||
---
|
||||
## 2) Hidden cross-service coupling (service imports service)
|
||||
- Where found:
|
||||
- [backend/app/services/jail_service.py](backend/app/services/jail_service.py)
|
||||
@@ -43,6 +20,7 @@
|
||||
- [Docs/Backend-Development.md](Docs/Backend-Development.md)
|
||||
|
||||
---
|
||||
|
||||
## 3) Blocklist import flow mixes too many responsibilities
|
||||
- Where found:
|
||||
- [backend/app/services/blocklist_service.py](backend/app/services/blocklist_service.py)
|
||||
@@ -62,6 +40,7 @@
|
||||
- [Docs/Features.md](Docs/Features.md)
|
||||
|
||||
---
|
||||
|
||||
## 4) Module-level mutable runtime flags in service layer
|
||||
- Where found:
|
||||
- [backend/app/services/jail_service.py](backend/app/services/jail_service.py)
|
||||
@@ -80,6 +59,7 @@
|
||||
- [Docs/Architekture.md](Docs/Architekture.md)
|
||||
|
||||
---
|
||||
|
||||
## 5) Inconsistent domain exception contracts across services
|
||||
- Where found:
|
||||
- [backend/app/routers/jails.py](backend/app/routers/jails.py)
|
||||
@@ -101,6 +81,7 @@
|
||||
- [Docs/Backend-Development.md](Docs/Backend-Development.md)
|
||||
|
||||
---
|
||||
|
||||
## 6) Raw DB connection exposed as dependency for all routes
|
||||
- Where found:
|
||||
- [backend/app/dependencies.py](backend/app/dependencies.py)
|
||||
@@ -119,6 +100,7 @@
|
||||
- [Docs/Backend-Development.md](Docs/Backend-Development.md)
|
||||
|
||||
---
|
||||
|
||||
## 7) Service layer coupled to response/presentation models
|
||||
- Where found:
|
||||
- [backend/app/services/ban_service.py](backend/app/services/ban_service.py)
|
||||
@@ -137,6 +119,7 @@
|
||||
- [Docs/Architekture.md](Docs/Architekture.md)
|
||||
|
||||
---
|
||||
|
||||
## 8) Inconsistent modeling style (TypedDict vs Pydantic)
|
||||
- Where found:
|
||||
- [backend/app/services/jail_service.py](backend/app/services/jail_service.py)
|
||||
@@ -156,6 +139,7 @@
|
||||
- [Docs/Backend-Development.md](Docs/Backend-Development.md)
|
||||
|
||||
---
|
||||
|
||||
## 9) Repository protocol coverage is incomplete
|
||||
- Where found:
|
||||
- [backend/app/repositories/protocols.py](backend/app/repositories/protocols.py)
|
||||
@@ -175,6 +159,7 @@
|
||||
- [Docs/Backend-Development.md](Docs/Backend-Development.md)
|
||||
|
||||
---
|
||||
|
||||
## 10) Startup sequence depends on implicit ordering
|
||||
- Where found:
|
||||
- [backend/app/startup.py](backend/app/startup.py)
|
||||
@@ -193,6 +178,7 @@
|
||||
- [Docs/Architekture.md](Docs/Architekture.md)
|
||||
|
||||
---
|
||||
|
||||
## 11) Logging semantics are inconsistent across backend modules
|
||||
- Where found:
|
||||
- [backend/app/services](backend/app/services)
|
||||
@@ -212,6 +198,7 @@
|
||||
- [Docs/Backend-Development.md](Docs/Backend-Development.md)
|
||||
|
||||
---
|
||||
|
||||
## 12) Prop drilling in jail overview page
|
||||
- Where found:
|
||||
- [frontend/src/pages/jails/JailOverviewSection.tsx](frontend/src/pages/jails/JailOverviewSection.tsx)
|
||||
@@ -231,6 +218,7 @@
|
||||
- [Docs/Web-Development.md](Docs/Web-Development.md)
|
||||
|
||||
---
|
||||
|
||||
## 13) Config page is over-centralized
|
||||
- Where found:
|
||||
- [frontend/src/pages/ConfigPage.tsx](frontend/src/pages/ConfigPage.tsx)
|
||||
@@ -249,6 +237,7 @@
|
||||
- [Docs/Web-Development.md](Docs/Web-Development.md)
|
||||
|
||||
---
|
||||
|
||||
## 14) Error boundary granularity is too coarse
|
||||
- Where found:
|
||||
- [frontend/src/App.tsx](frontend/src/App.tsx)
|
||||
@@ -269,6 +258,7 @@
|
||||
- https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary
|
||||
|
||||
---
|
||||
|
||||
## 15) Fragmented async error UX handling in components
|
||||
- Where found:
|
||||
- [frontend/src/pages/jails/BanUnbanForm.tsx](frontend/src/pages/jails/BanUnbanForm.tsx)
|
||||
@@ -288,6 +278,7 @@
|
||||
- [Docs/Web-Development.md](Docs/Web-Development.md)
|
||||
|
||||
---
|
||||
|
||||
## 16) API usage pattern is inconsistent across components/hooks
|
||||
- Where found:
|
||||
- [frontend/src/pages/JailsPage.tsx](frontend/src/pages/JailsPage.tsx)
|
||||
@@ -307,6 +298,7 @@
|
||||
- [Docs/Web-Development.md](Docs/Web-Development.md)
|
||||
|
||||
---
|
||||
|
||||
## 17) Weak typed error contracts in generic hooks
|
||||
- Where found:
|
||||
- [frontend/src/hooks/useListData.ts](frontend/src/hooks/useListData.ts)
|
||||
@@ -326,6 +318,7 @@
|
||||
- [frontend/src/api/client.ts](frontend/src/api/client.ts)
|
||||
|
||||
---
|
||||
|
||||
## 18) Duplicate polling/list loading behavior across hooks
|
||||
- Where found:
|
||||
- [frontend/src/hooks/useListData.ts](frontend/src/hooks/useListData.ts)
|
||||
@@ -344,6 +337,7 @@
|
||||
- [Docs/Web-Development.md](Docs/Web-Development.md)
|
||||
|
||||
---
|
||||
|
||||
## 19) Provider dependency chain is implicit
|
||||
- Where found:
|
||||
- [frontend/src/App.tsx](frontend/src/App.tsx)
|
||||
@@ -362,6 +356,7 @@
|
||||
- [Docs/Web-Development.md](Docs/Web-Development.md)
|
||||
|
||||
---
|
||||
|
||||
## 20) Loading UX lacks progressive/skeleton states
|
||||
- Where found:
|
||||
- [frontend/src/pages](frontend/src/pages)
|
||||
@@ -379,6 +374,7 @@
|
||||
- [Docs/Web-Design.md](Docs/Web-Design.md)
|
||||
|
||||
---
|
||||
|
||||
## 21) Silent auth error swallow in fetch error utility
|
||||
- Where found:
|
||||
- [frontend/src/utils/fetchError.ts](frontend/src/utils/fetchError.ts)
|
||||
@@ -396,6 +392,7 @@
|
||||
- [frontend/src/providers/AuthProvider.tsx](frontend/src/providers/AuthProvider.tsx)
|
||||
|
||||
---
|
||||
|
||||
## 22) Magic strings are scattered in frontend storage keys
|
||||
- Where found:
|
||||
- [frontend/src/providers/AuthProvider.tsx](frontend/src/providers/AuthProvider.tsx)
|
||||
@@ -415,6 +412,7 @@
|
||||
- [frontend/src/utils/constants.ts](frontend/src/utils/constants.ts)
|
||||
|
||||
---
|
||||
|
||||
## 23) No global cancellation policy on route transitions
|
||||
- Where found:
|
||||
- [frontend/src/hooks](frontend/src/hooks)
|
||||
@@ -432,6 +430,7 @@
|
||||
- [Docs/Web-Development.md](Docs/Web-Development.md)
|
||||
|
||||
---
|
||||
|
||||
## 24) API response wrapper shape is inconsistent
|
||||
- Where found:
|
||||
- [backend/app/routers/dashboard.py](backend/app/routers/dashboard.py)
|
||||
@@ -452,6 +451,7 @@
|
||||
- [Docs/Backend-Development.md](Docs/Backend-Development.md)
|
||||
|
||||
---
|
||||
|
||||
## 25) No canonical snake_case/camelCase serialization policy
|
||||
- Where found:
|
||||
- [backend/app/models/server.py](backend/app/models/server.py)
|
||||
@@ -471,6 +471,7 @@
|
||||
- https://docs.pydantic.dev/latest/concepts/alias/
|
||||
|
||||
---
|
||||
|
||||
## 26) Pagination contract is not standardized across endpoints
|
||||
- Where found:
|
||||
- [backend/app/routers/dashboard.py](backend/app/routers/dashboard.py)
|
||||
@@ -490,6 +491,7 @@
|
||||
- [Docs/Backend-Development.md](Docs/Backend-Development.md)
|
||||
|
||||
---
|
||||
|
||||
## 27) Error response body shape is inconsistent
|
||||
- Where found:
|
||||
- [backend/app/main.py](backend/app/main.py)
|
||||
@@ -509,6 +511,7 @@
|
||||
- [Docs/Backend-Development.md](Docs/Backend-Development.md)
|
||||
|
||||
---
|
||||
|
||||
## 28) Login failure delay can enable app-layer DoS
|
||||
- Where found:
|
||||
- [backend/app/routers/auth.py](backend/app/routers/auth.py#L110)
|
||||
@@ -526,6 +529,7 @@
|
||||
- [backend/app/utils/rate_limiter.py](backend/app/utils/rate_limiter.py)
|
||||
|
||||
---
|
||||
|
||||
## 29) Blocklist URL validation has DNS-rebinding window
|
||||
- Where found:
|
||||
- [backend/app/utils/ip_utils.py](backend/app/utils/ip_utils.py#L145)
|
||||
@@ -545,6 +549,7 @@
|
||||
- https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html
|
||||
|
||||
---
|
||||
|
||||
## 30) Setup persistence is non-atomic across DB contexts
|
||||
- Where found:
|
||||
- [backend/app/services/setup_service.py](backend/app/services/setup_service.py)
|
||||
@@ -563,6 +568,7 @@
|
||||
- [Docs/Architekture.md](Docs/Architekture.md)
|
||||
|
||||
---
|
||||
|
||||
## 31) Fire-and-forget reschedule may fail silently
|
||||
- Where found:
|
||||
- [backend/app/tasks/blocklist_import.py](backend/app/tasks/blocklist_import.py#L108)
|
||||
@@ -580,6 +586,7 @@
|
||||
- [Docs/Features.md](Docs/Features.md)
|
||||
|
||||
---
|
||||
|
||||
## 32) RateLimiter cleanup function is not scheduled/used
|
||||
- Where found:
|
||||
- [backend/app/utils/rate_limiter.py](backend/app/utils/rate_limiter.py#L84)
|
||||
@@ -598,6 +605,7 @@
|
||||
- [backend/app/utils/rate_limiter.py](backend/app/utils/rate_limiter.py)
|
||||
|
||||
---
|
||||
|
||||
## 33) Trusted proxy configuration is hardcoded in auth router
|
||||
- Where found:
|
||||
- [backend/app/routers/auth.py](backend/app/routers/auth.py#L46)
|
||||
@@ -617,6 +625,7 @@
|
||||
- [Docs/Instructions.md](Docs/Instructions.md)
|
||||
|
||||
---
|
||||
|
||||
## 34) Setup redirect allowlist uses broad prefix matching
|
||||
- Where found:
|
||||
- [backend/app/main.py](backend/app/main.py#L434)
|
||||
@@ -634,6 +643,7 @@
|
||||
- [backend/app/main.py](backend/app/main.py)
|
||||
|
||||
---
|
||||
|
||||
## 35) API client sends JSON and CSRF header for every request method
|
||||
- Where found:
|
||||
- [frontend/src/api/client.ts](frontend/src/api/client.ts)
|
||||
@@ -652,6 +662,7 @@
|
||||
- [backend/app/middleware/csrf.py](backend/app/middleware/csrf.py)
|
||||
|
||||
---
|
||||
|
||||
## 36) Polling continues when tab is not visible
|
||||
- Where found:
|
||||
- [frontend/src/hooks/usePolledData.ts](frontend/src/hooks/usePolledData.ts#L90)
|
||||
@@ -670,6 +681,7 @@
|
||||
- [Docs/Web-Development.md](Docs/Web-Development.md)
|
||||
|
||||
---
|
||||
|
||||
## 37) Multi-worker safety check depends on one environment variable
|
||||
- Where found:
|
||||
- [backend/app/startup.py](backend/app/startup.py#L61)
|
||||
@@ -687,6 +699,7 @@
|
||||
- [Docs/Architekture.md](Docs/Architekture.md)
|
||||
|
||||
---
|
||||
|
||||
## 38) History archive query paths may need explicit indexing plan
|
||||
- Where found:
|
||||
- [backend/app/db.py](backend/app/db.py)
|
||||
@@ -707,6 +720,7 @@
|
||||
- https://www.sqlite.org/queryplanner.html
|
||||
|
||||
---
|
||||
|
||||
## 39) No explicit DI container strategy for backend service graph
|
||||
- Where found:
|
||||
- [backend/app/dependencies.py](backend/app/dependencies.py)
|
||||
@@ -725,6 +739,7 @@
|
||||
- [Docs/Architekture.md](Docs/Architekture.md)
|
||||
|
||||
---
|
||||
|
||||
## 40) Frontend and backend observability are not aligned
|
||||
- Where found:
|
||||
- [backend/app/main.py](backend/app/main.py)
|
||||
@@ -741,4 +756,4 @@
|
||||
- Add observability and privacy-safe logging guidelines.
|
||||
- Doc references:
|
||||
- [Docs/Architekture.md](Docs/Architekture.md)
|
||||
- [Docs/Web-Development.md](Docs/Web-Development.md)
|
||||
- [Docs/Web-Development.md](Docs/Web-Development.md)
|
||||
Reference in New Issue
Block a user