Skip to content

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

  1. Is this module at the observability or config layer? → it imports nothing from other Athomic modules.
  2. Does this import go from a lower layer to a higher layer? → STOP. Invert via Protocol.
  3. Am I about to write TYPE_CHECKING? → STOP. Restructure the architecture.
  4. 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 —

  • Violations backlog: docs/backlog/todo/solid-violations.md
  • Memory rule (persists across sessions): .claude/projects/.../memory/solid_architecture_rules.md