Architecture Rules
Clean Architecture guidelines for WordLoop Python Core
Architecture Rule: Clean Architecture for WordLoop ML
1. Context & Scope
This rule physically applies our Service Architecture Principles to all Python code within src/wordloop.
We divide the code structurally based on its behavior: the core business logic remains isolated in src/wordloop/core/, inbound traffic is handled by src/wordloop/entrypoints/, and all external integrations belong in src/wordloop/providers/.
2. Architectural Layers (Inward Dependency Flow)
Dependencies must only point INWARD. Inner layers must never import from outer layers.
Domain (src/wordloop/core/domain)
- Purpose: Business entities and core logic using
dataclassesorPydantic. - Zero-Dependency Core: Standard library and Pydantic/Dataclasses only. No I/O.
- Framework Agnostic Definitions: Models must remain free from database-specific decorators or library-specific types (e.g., avoid
SQLAlchemyORM models here). - Universal Vocabulary: Define core application exceptions (
src/wordloop/core/exceptions.py) and constants here. - Testing: Pure Unit Tests. Verify state transitions and business rules with zero mocks.
Gateways (src/wordloop/core/gateways)
- Purpose:
typing.Protocolorabc.ABCdefinitions that define capabilities. - Contractual Masters: Gateways define what (e.g.,
store), never how. - No Leaky Abstractions: Signatures must use Domain entities. Never reference SDK types (e.g.,
openai.ChatCompletion) or transport types. - The Golden Rule: Use generic names.
publish(msg: Message), notsend_to_sqs(msg: Message).
Services (src/wordloop/core/services)
- Purpose: Use-case orchestration. This is where the application "decides" what happens.
- Dependency Injection: Services depend on Gateways (Protocols/ABCs), not concrete Providers.
- Protocol Consumer Rule: Services should return concrete Domain objects. If a Service is used by an Entrypoint, the Entrypoint defines the Protocol/Interface it requires from the Service.
- Transaction Boundaries: Coordinate workflows by fetching data, applying domain logic, and persisting results.
Providers (src/wordloop/providers/)
- Purpose: Concrete implementations of Gateway interfaces (The "Adapter").
- Mapping (Domain Alignment): Translates external SDK responses into Domain Entities.
- Error Wrapping: Catch library-specific errors (e.g.,
botocore.exceptions.ClientError) and raise a corresponding Core Exception defined in the Domain layer. - Testing: Integration Tests only. Use
testcontainers-pythonto verify actual I/O against real instances.
Entrypoints (src/wordloop/entrypoints/)
- Purpose: The interaction layer (FastAPI, CLI).
- Boundary Validation: Entrypoints validate inputs, map request schemas to Domain objects, and call a Service. They delegate all business decisions to the Core.
- Testing: Use
fastapi.testclient.TestClientwith mocked Services to verify routing and status codes.
2a. Worked Example: Live Streaming
StreamingSessionService (src/wordloop/core/services/streaming_session_service.py) is the streaming entry point — every live-recording audio frame from Core, and every diagnostic decision about it, flows through this one service. It illustrates the layering above with a concrete, non-trivial example:
- Entrypoint —
streaming_router(src/wordloop/entrypoints/api/streaming_api.py) validates the HTTP/WebSocket boundary and callsStreamingSessionService. It never makes a health or scoring decision itself. - Service —
StreamingSessionServiceowns the use case: gap-tolerant sequence tracking, a bounded queue drained by a dedicated worker thread (so the HTTP handler never blocks on the provider SDK), and the no-audio detection state machine described in Pipelines. - Gateways — the service depends on
StreamingTranscriptionGateway(the upstream transcription provider),RecordingHealthGateway(reports health signals to Core),AudioCodecGateway(decodes containerised audio for scoring), andWordLoopCoreGateway(rebuilds insight context from Core on resume) — alltyping.Protocoldefinitions the service depends on, never a concrete provider. - Providers —
PydubAudioCodecProvideris the soleAudioCodecGatewayimplementation; it consolidates every pydub/ffmpeg/magic call in the codebase behind that one contract.
RecordingHealthGateway
class RecordingHealthGateway(Protocol):
def report(self, signal: RecordingHealthSignal) -> None: ...A health signal is advisory: report must be non-blocking and failure-tolerant. A failed delivery is logged and never raised into the audio path — a Core outage must not stop live audio from reaching the transcription provider.
AudioCodecGateway.decode_pcm16
class AudioCodecGateway(Protocol):
def decode_pcm16(self, chunk: bytes) -> bytes | None: ...Unlike to_pcm16 (which transcodes and, for unrecognised bytes, assumes raw PCM), decode_pcm16 never guesses: it returns None when the chunk has no decodable container, so a caller can distinguish "this is raw PCM" from "this is bytes I can't identify" — a distinction the no-audio detector depends on to avoid scoring silence into unknown data.
3. Dependency Injection & State
- Constructor Injection: Use
__init__for all dependencies. - Explicit Lifecycles: Initialize database clients solely at the entrypoint startup (e.g.,
lifespanin FastAPI) and inject them rather than relying on global singletons. - Wiring: All concrete Provider-to-Service wiring happens at the outermost edge (the Entrypoint or a dedicated
container.py).
4. Integrity & System Testing
Bootstrap Verification (The "Smoke" Test)
To ensure the application is wired correctly:
- Wiring Test: A test in
tests/system/test_bootstrap.pythat attempts to initialize the full dependency tree. - Validation: Ensures that all required environment variables are present and that the DI container (or manual wiring) doesn't fail on startup.
Golden Path System Tests
- Location:
tests/system/. - Strategy: Run a live instance of the app (e.g., using
uvicornin a subprocess orTestClientwith real providers) against real infrastructure viatestcontainers. - Zero Mocks: These tests verify the "Golden Thread" from the API route all the way to the database/third-party SDK.
- Scope: Focus strictly on high-value success paths.
4a. Service-to-Service Authentication
Every route under /streaming/* and both /transcription-jobs/* routes require the X-Service-Token header (require_service_token in src/wordloop/entrypoints/api/auth.py, applied at the router level via dependencies=[Depends(require_service_token)]). The dependency compares the header with hmac.compare_digest against the configured token: 401 on missing/mismatch, 503 when no token is configured — it fails closed, never open. create_app calls assert_service_token_configured at startup and refuses to serve traffic if the token is empty outside {test, local, development, dev}.
GET /healthz, GET /docs, and GET /openapi.json are deliberately left open (liveness probes and schema-only endpoints carry no tenant data). /voice/* and /chat are not yet gated — Core's VoiceClient does not send the header today, so gating them would break the caller before it is wired to send one.
5. Core Engineering Standards
- Pydantic Everywhere: Use Pydantic for all data boundaries (Request/Response and Domain).
- Structured Logging: Utilize structural logging libraries rather than basic print statements to ensure trace fidelity.
- Acyclic Dependencies: Prevent import cycles, as they act as an immediate signal of leaked layer responsibilities.
- Strict Boundaries: Maintain clean layers by preventing FastAPI
Depends,Request, or SQLSessionobjects from entering the Service or Domain layers. - Clean Containers: Always ensure
container.stop()or similar cleanup is called inpytestfixtures to prevent resource leaks in CI.