WordloopWordloop
LearnPlatform ServicesML (Python)

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 dataclasses or Pydantic.
  • 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 SQLAlchemy ORM 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.Protocol or abc.ABC definitions 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), not send_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-python to 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.TestClient with 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 calls StreamingSessionService. It never makes a health or scoring decision itself.
  • Service — StreamingSessionService owns 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), and WordLoopCoreGateway (rebuilds insight context from Core on resume) — all typing.Protocol definitions the service depends on, never a concrete provider.
  • Providers — PydubAudioCodecProvider is the sole AudioCodecGateway implementation; 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., lifespan in 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.py that 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 uvicorn in a subprocess or TestClient with real providers) against real infrastructure via testcontainers.
  • 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

  1. Pydantic Everywhere: Use Pydantic for all data boundaries (Request/Response and Domain).
  2. Structured Logging: Utilize structural logging libraries rather than basic print statements to ensure trace fidelity.
  3. Acyclic Dependencies: Prevent import cycles, as they act as an immediate signal of leaked layer responsibilities.
  4. Strict Boundaries: Maintain clean layers by preventing FastAPI Depends, Request, or SQL Session objects from entering the Service or Domain layers.
  5. Clean Containers: Always ensure container.stop() or similar cleanup is called in pytest fixtures to prevent resource leaks in CI.

On this page