Implicit Conventions¶
CLAUDE.md codifies the rules we enforce with hooks and review agents.
This page captures the implicit conventions: patterns followed
across the codebase by precedent rather than enforcement. New code
should follow them; deviations should be justified in the diff.
1. Repository CRUD signature pattern¶
Every repository protocol exposes the same five-method shape, returning
bool from delete so callers can distinguish "removed one row" from
"id did not exist" without raising.
class ApprovalRepository(Protocol):
async def save(self, item: ApprovalItem) -> None: ...
async def get(self, approval_id: NotBlankStr) -> ApprovalItem | None: ...
async def list_items(
self,
*,
status: ApprovalStatus | None = None,
limit: int | None = None,
offset: int = 0,
) -> tuple[ApprovalItem, ...]: ...
async def delete(self, approval_id: NotBlankStr) -> bool: ...
Reference: src/synthorg/persistence/approval_protocol.py:34-73.
2. Service lifecycle method symmetry¶
Long-lived services own a private asyncio.Lock named
_lifecycle_lock (separate from any hot-path lock) and expose
symmetric async start() / async stop() methods. Both must be
held across the full body of their respective method per
lifecycle-sync.md. Examples:
src/synthorg/workers/worker.py,
src/synthorg/integrations/health/prober.py.
3. API response wrapping¶
Controllers return one of three shapes:
ApiResponse[T]: success-only path with no header or status-code customization. Litestar serializes it as{"data": ..., "error": null, "success": true}.PaginatedResponse[T]: list endpoints that return a page of items plus pagination metadata. WrapsApiResponse[T]and adds apaginationenvelope ({limit,offset,total,next_cursor,has_more}). Required for any controller method whose return type is a collection. Opaque cursor pagination is the project default.Response[ApiResponse[T]]: only when status code or response headers must be customized (e.g. setting aLocationheader on a 201, attaching aRetry-Afterheader on a 429).
Reference: src/synthorg/api/dto.py (ApiResponse,
PaginatedResponse, PaginationMeta); almost every controller under
src/synthorg/api/controllers/.
4. @model_validator(mode="after") is the default¶
mode="after" runs against the constructed model and is the default
choice. mode="before" is reserved for normalizing inputs the caller
might pass in non-canonical shape (lists vs tuples, dirty strings,
missing aliases). When using mode="before", never mutate the
input dict in place; return a new dict via {**data, key: value}.
The four sites flagged by the 2026-04-25 audit have been converted;
the new immutability tests in tests/unit/{api,tools}/... lock
in the pattern.
5. Event constant module imports¶
Every observability event is defined as a Final[str] constant under
src/synthorg/observability/events/<domain>.py and imported by name
(never by string literal) from the consumer.
from synthorg.observability.events.workers import (
WORKERS_DISPATCHER_CLAIM_ENQUEUED,
WORKERS_DISPATCHER_PUBLISH_EXHAUSTED,
WORKERS_DISPATCHER_PUBLISH_FAILED,
WORKERS_DISPATCHER_PUBLISH_RETRYING,
WORKERS_DISPATCHER_QUEUE_NOT_RUNNING,
)
Reference: src/synthorg/workers/dispatcher.py:19-25.
6. Domain error hierarchies¶
Each domain owns errors.py with a base error class carrying
status_code / error_code / error_category / retryable as
ClassVars; subclasses inherit and override only the fields that
change. The HTTP exception handler keys off the base class so a new
subclass automatically inherits the correct status mapping.
References:
src/synthorg/budget/errors.py:BudgetExhaustedErrorfamily.src/synthorg/communication/errors.py:CommunicationErrorfamily.src/synthorg/engine/errors.py:EngineErrorfamily.
7. Module file structure¶
Every business-logic module follows the same top-down ordering:
- Module docstring.
- Imports: stdlib, then third-party, then internal, alphabetical within each group.
logger = get_logger(__name__)immediately after imports.- Module-level
Finalconstants (private prefixed with_). - Public types (Pydantic models, dataclasses, enums).
- Public functions / classes.
- Private helpers (prefixed with
_).
Reference: src/synthorg/communication/bus/memory.py.
8. Frozen ConfigDict pattern¶
Every Pydantic model declares
model_config = ConfigDict(frozen=True, allow_inf_nan=False). Request
DTOs additionally set extra="forbid" so unknown keys are rejected
instead of silently ignored. Combined with the framework's frozen
guarantee this gives us the "create new objects, never mutate
existing ones" property the immutability covenant relies on.
References: 30+ occurrences across src/synthorg/. Canonical example:
src/synthorg/approval/models.py:28.
See also¶
- persistence-boundary.md: repository / service / controller layering.
- lifecycle-sync.md:
_lifecycle_lockrule. - pluggable-subsystems.md: protocol + strategy + factory + config discriminator pattern.