SOLID Architecture Rules — Athomic
Status: MANDATORY. Every file written or reviewed must comply with these rules. Violations are tracked in
docs/backlog/todo/solid-violations.md.
Why This Document Exists
A TYPE_CHECKING block was found in observability/metrics/usage/ai/features/feature_metrics.py importing from ai.features.quality.protocol. The observability layer — which is infrastructure — was depending on the AI domain layer, inverting the dependency arrow. This is a DIP violation and it couples two modules that must be independent.
TYPE_CHECKING is never acceptable in this codebase. It is a cover-up for an architectural problem, not a solution.
The 5 SOLID Principles — Applied to This Codebase
S — Single Responsibility Principle
Each module has one reason to change.
| Module | Its reason to change |
|---|---|
observability/metrics/ |
Prometheus metric definitions change |
ai/features/encoders/ |
Feature encoding logic changes |
ai/features/encoders/telemetry.py |
Drift recording logic changes |
database/ |
Storage backend changes |
resilience/ |
Resilience policy changes |
Violation indicator: A module that imports from 3+ unrelated layers has too many reasons to change.
# ❌ WRONG — feature_metrics.py mixing metric definitions with domain logic
class DriftStatsProvider(Protocol): ... # domain concern
def record_output_distribution(...): ... # AI domain logic
feature_encoding_duration_seconds = Histogram # metric definition
# ✅ CORRECT
# feature_metrics.py → ONLY metric object definitions
# encoders/telemetry.py → recording logic (owns AI domain types)
O — Open/Closed Principle
Extend via Protocols and Registries, never by modifying core.
# ❌ WRONG — adding a branch to handle a new case
def execute(engine):
if isinstance(engine, NumpyEngine):
...
elif isinstance(engine, TorchEngine): # modifying core
...
# ✅ CORRECT — register a new provider, core never changes
class TorchEngine(FeatureExecutionEngineProtocol):
async def execute_tensor_batch(...): ...
EngineRegistry.register("torch", TorchEngine)
L — Liskov Substitution Principle
Every Protocol implementation must be fully substitutable.
# ❌ WRONG — checking the concrete type breaks substitutability
def compress(provider: CompressionProtocol) -> bytes:
if isinstance(provider, GzipProvider): # violates LSP
return provider.gzip_compress(data)
return provider.compress(data)
# ✅ CORRECT — call the Protocol method, trust the contract
def compress(provider: CompressionProtocol) -> bytes:
return provider.compress(data)
I — Interface Segregation Principle
Protocols expose only the methods the consumer needs.
# ❌ WRONG — importing a fat class for one method
from nala.athomic.ai.features.quality.protocol import DriftBaselineProtocol
# ^ 10 methods, we need 1
# ✅ CORRECT — define a minimal protocol at the consumer's layer
class DriftStatsProvider(Protocol):
def get_feature_stats(self, vector_id: str, feature_key: str) -> Tuple[Optional[float], Optional[float]]: ...
D — Dependency Inversion Principle (THE MOST CRITICAL)
High-level modules NEVER depend on low-level modules. Both depend on abstractions.
The abstraction (Protocol) is defined at the lower layer, not the higher one.
Layer Order — LOW → HIGH (higher layers MAY import lower, NEVER the reverse)
observability/ ← ABSOLUTE BOTTOM — zero imports from any Athomic module
config/schemas/ ← Pure Pydantic — zero imports from any Athomic module
resilience/ ← may import: observability, config
security/ ← may import: observability, config, resilience
database/ ← may import: observability, config
performance/ ← may import: observability, config, database, resilience
serializer/ ← may import: observability, config
integration/ ← may import: observability, config, database, serializer, security, resilience, performance
streaming/ ← may import: observability, config, database, integration
event_sourcing/ ← may import: observability, config, database
workflows/ ← may import: observability, config, database, integration, ai
plugins/ ← may import: observability, config — NEVER facade
ai/ ← may import: observability, config, database, resilience, performance
facade/ ← COMPOSITION ROOT — imports everything, wires everything
api/ ← may import: facade, observability
DIP Violation Examples
# ❌ FORBIDDEN — observability importing AI domain
# observability/metrics/feature_metrics.py
from nala.athomic.ai.features.quality.protocol import DriftBaselineProtocol
# ❌ FORBIDDEN — plugins importing the composition root
# plugins/hooks.py
from nala.athomic.facade import Athomic
# ❌ FORBIDDEN — database importing AI
# database/mongo.py
from nala.athomic.ai.features.types import FeatureDict
# ✅ CORRECT — AI importing observability (low layer)
# ai/features/encoders/telemetry.py
from nala.athomic.observability.metrics.usage.ai.features import feature_output_distribution
# ✅ CORRECT — define Protocol at the low layer so AI can implement it without coupling
# observability/metrics/some_module.py
class SomeStatsProvider(Protocol):
def get_stats(self, key: str) -> float: ...
ABSOLUTE FORBIDDEN PATTERNS
1. TYPE_CHECKING — BANNED COMPLETELY IN THIS CODEBASE
# ❌ NEVER — this is a gambiarra that hides a broken architecture
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from nala.athomic.ai.features.quality.protocol import DriftBaselineProtocol
Why it's wrong: - The import only runs during type checking, masking a real circular/inverted dependency at runtime - It creates invisible coupling: the module "doesn't import" at runtime but the architecture is wrong - Mypy still validates the type — so the coupling is real
The real fix: Move the code to the correct layer OR define a local Protocol.
# ✅ CORRECT option A — move the function UP to the layer that owns the domain type
# ai/features/encoders/telemetry.py
from ..quality.protocol import DriftBaselineProtocol # same layer, correct direction
# ✅ CORRECT option B — define a minimal Protocol at the lower layer
# observability/metrics/some_module.py
class SomeStatsProvider(Protocol):
def get_stats(self, key: str) -> float: ...
# The higher layer implements it structurally — no coupling needed
2. observability/ importing from any other Athomic module — BANNED
# ❌ FORBIDDEN — in any file under observability/
from nala.athomic.ai.features.quality.protocol import X
from nala.athomic.database.kvstore.protocol import Y
from nala.athomic.resilience.backpressure.manager import Z
observability/ must be importable in complete isolation. It is the foundation.
3. config/schemas/ importing from any other Athomic module — BANNED
# ❌ FORBIDDEN — config schemas are pure Pydantic, no runtime deps
from nala.athomic.ai.features.types import FeatureValue
4. plugins/ importing from facade/ — BANNED
# ❌ FORBIDDEN
from nala.athomic.facade import Athomic
# ✅ CORRECT — define AthomicProtocol in a shared interfaces/ module
# plugins/interfaces.py
class AthomicProtocol(Protocol):
def get_component(self, name: str) -> Any: ...
5. Any module importing from a higher layer — BANNED
# ❌ FORBIDDEN — resilience importing AI
from nala.athomic.ai.features.something import SomeThing
# ❌ FORBIDDEN — database importing resilience
from nala.athomic.resilience.circuit_breaker import CircuitBreaker
6. Circular imports (within the same package) — BANNED
Using TYPE_CHECKING to paper over circular imports is doubly banned.
# ❌ WRONG — circular: A imports B, B imports A, workaround with TYPE_CHECKING
# module_a.py
if TYPE_CHECKING:
from .module_b import B
# ✅ CORRECT — extract shared types to module_c that both A and B import
# shared_types.py ← new file, no imports from A or B
class SharedBase(Protocol): ...
# module_a.py
from .shared_types import SharedBase
# module_b.py
from .shared_types import SharedBase
How to Identify Violations
Checklist Before Writing Any Import
- Is this module at the observability or config layer? → it imports nothing from other Athomic modules.
- Does this import go from a lower layer to a higher layer? → STOP. Invert via Protocol.
- Am I about to write
TYPE_CHECKING? → STOP. Restructure the architecture. - Is this a circular import within the same package? → Extract shared types to a third module.
Quick Layer Check
# When writing: from nala.athomic.X import Y
# Ask: is my current file in a LOWER layer than X?
# If yes → FORBIDDEN. Define a Protocol here instead.
Code Smell Detector
A pre-commit hook runs scripts/check_solid.py to catch violations before commit.
It detects:
- TYPE_CHECKING usage anywhere in the codebase
- observability/ importing from other nala.athomic.* modules
- config/schemas/ importing from other nala.athomic.* modules
- plugins/ importing from facade
Run manually:
python scripts/check_solid.py
Module-Specific Rules
| Module | Can import from | Cannot import from | Rule |
|---|---|---|---|
observability/ |
stdlib, prometheus, loguru, opentelemetry | Any nala.athomic.* |
SOLID-002 |
config/schemas/ |
stdlib, pydantic | Any nala.athomic.* |
SOLID-003 |
resilience/ |
observability, config | ai, facade | SOLID-005 |
security/ |
observability, config, resilience | ai, integration, performance, facade | SOLID-008 |
database/ |
observability, config | ai, resilience, facade | SOLID-006 |
performance/ |
observability, config, database, resilience | ai, integration, facade | SOLID-009 |
serializer/ |
observability, config | ai, database, resilience, facade | — |
integration/ |
observability, config, database, serializer, security, resilience, performance | ai, facade | SOLID-007 |
streaming/ |
observability, config, database, integration | ai, facade | SOLID-010 |
event_sourcing/ |
observability, config, database | ai, facade | SOLID-011 |
workflows/ |
observability, config, database, integration, ai | facade | SOLID-012 |
plugins/ |
observability, config | facade, ai, database | SOLID-004 |
ai/ |
observability, config, database, resilience, performance | facade, api | — |
facade/ |
Everything | Nothing (it IS the root) | — |
api/ |
facade, observability | Direct lower-layer modules | — |
Related Documents
- Violations backlog:
docs/backlog/todo/solid-violations.md - Memory rule (persists across sessions):
.claude/projects/.../memory/solid_architecture_rules.md