Skip to content

background design decisions

github-actions[bot] edited this page Jun 19, 2026 · 1 revision

Design decisions

The four ADRs in docs/adrs/ document architectural decisions accepted by the 360Ghar team. All four were written on 2026-05-08 (ADR 004 revised 2026-06-06). They describe a target architecture the codebase is migrating toward - the current type-based layout in app/api/, app/services/, app/models/ is what ADR 001 proposes to move away from.

Active contributors: Saksham, Ravi

ADR 001: Domain-Driven Module Structure

File: docs/adrs/001-domain-module-structure.md

Problem. The current type-based (layered) directory layout organizes code by technical role rather than business domain. A change to the booking flow touches bookings.py in endpoints, booking.py in services, models in models/, schemas in schemas/, and MCP tools in mcp/tool_ops/. With 40+ service files and 30+ schema modules in flat directories, finding the relevant file for a domain concept requires knowing the project's naming conventions. MCP tool logic lives in app/mcp/tool_ops/ while the same domain's REST endpoint lives elsewhere, leading to duplicated or divergent authorization.

Decision. Migrate to a domain-driven module structure under app/modules/{domain}/ where each module is self-contained: api.py, service.py, repository.py, models.py, schemas.py, mcp.py, enums.py, and a tests/ directory. Sub-domains (like PM under property) nest further. app/shared/ holds cross-domain contracts.

Status. Accepted but not implemented. app/modules/ and app/shared/ exist as reserved placeholders. The current concrete homes (app/api, app/services, app/models, app/repositories, app/mcp) remain canonical until a domain is migrated.

ADR 002: Repository Pattern with Protocol Interfaces

File: docs/adrs/002-repository-protocol-interfaces.md

Problem. The repository pattern is incomplete. app/repositories/ has BaseRepository[T], PropertyRepository, and PropertyQueryBuilder, but most services (booking.py, visit.py, flatmates.py, user.py) write raw SQLAlchemy queries directly against AsyncSession. This couples services to SQLAlchemy, makes unit testing impossible without a real database, and duplicates query logic (filter by user ID, paginate, load relationships) across services.

Decision. Introduce typing.Protocol-based repository interfaces that define the data access contract for each domain. Services depend on protocols, not on concrete SQLAlchemy repositories. Concrete implementations wrap SQLAlchemy and live in the repository layer. This creates a clean seam for swapping persistence or inserting a caching layer.

Status. Accepted. BaseRepository and PropertyRepository already follow this pattern for the property domain; other domains have not been migrated.

ADR 003: Event-Driven Side Effects

File: docs/adrs/003-event-driven-side-effects.md

Problem. Business operations trigger side effects (push notifications, emails, cache invalidation, analytics) that are executed inline as sequential in-service function calls. The notification dispatcher blocks the primary operation's response - if email or FCM is slow, the HTTP response to the client is delayed. Error handling is inconsistent across call sites. There is no ordering guarantee, no deduplication, no replay, and unit tests must mock every side effect.

Decision. Introduce a simple in-process event bus that decouples primary business operations from their side effects. Operations emit events; registered handlers react asynchronously in background tasks. Handler failures are isolated from the primary operation. Optional Redis pub/sub extension for multi-process dispatch.

Status. Accepted. The SSEEventBus in app/core/sse.py is a per-user pub/sub for real-time UI updates, not the general-purpose event bus this ADR describes. The notification dispatcher (app/services/notification_dispatcher.py) still runs inline. Migration to the event bus is pending.

ADR 004: Adapter Pattern for External Services

File: docs/adrs/004-external-service-adapters.md

Problem. The backend integrates with many external HTTP services (Gemini, GLM, Supabase, FCM, email, SMS, Cloudinary, 15+ data hub scrapers). The AI provider layer (app/services/ai/) already demonstrates a well-structured adapter pattern with AIProvider, GeminiProvider, GLMProvider, get_ai_provider(), and centralized tenacity retries - but other services implement their own error handling inconsistently, some with no retries. There is no circuit breaking: if Gemini is down, every request still tries it and waits for the full timeout cycle. HTTP client setup is duplicated.

Decision. Formalize the adapter pattern for all external service integrations. Each external service is wrapped behind an adapter interface that centralizes retry, circuit-breaking, timeout, and health tracking. Business code depends on adapter protocols, not on raw HTTP clients. The shared httpx.AsyncClient singletons in app/core/http.py (get_scraper_client, get_blog_client, get_general_client, get_supabase_auth_http_client) are a step in this direction.

Status. Accepted. The shared httpx clients exist and the AI provider factory is the reference implementation. Other services (email, SMS, FCM, scrapers) have not been migrated to the adapter pattern.

Cross-cutting theme

All four ADRs push in the same direction: decouple. Decouple domains from each other (ADR 001), services from persistence (ADR 002), primary operations from side effects (ADR 003), and business code from external services (ADR 004). The codebase has the seams in place (app/modules/, app/shared/, BaseRepository, SSEEventBus, shared httpx clients) but the migration is incremental.

Clone this wiki locally