Skip to main content

ADR-015: Lazy Import Patterns

Status: Accepted Date: 2026-01-18 Authors: Development Team

Context

Aragora is a large codebase with complex interdependencies between modules. As the system grew, we encountered circular import issues where module A imports module B, and module B imports module A, causing ImportError at startup.

Additionally, some modules have heavy initialization costs (database connections, model loading) that we want to defer until actually needed.

Decision

We adopt lazy import patterns to:

  1. Break circular dependencies
  2. Reduce startup time
  3. Make optional dependencies truly optional

Pattern 1: Function-Level Import (Circular Dependencies)

Import inside the function that needs it:

def some_function():
# Import here to avoid circular dependency
from aragora.other_module import SomeClass
return SomeClass()

Used in:

  • aragora/privacy/isolation.py:283
  • aragora/training/specialist_models.py:574
  • aragora/agents/specialist_factory.py:187, 222
  • aragora/agents/fallback.py:179
  • aragora/server/middleware/cache.py:36

Pattern 2: Lazy Module Loader (Heavy Dependencies)

Create a wrapper that imports on first access:

def _get_handlers_base():
"""Lazy import of handlers/base.py to avoid circular imports."""
from aragora.server.handlers import base
return base

Used in:

  • aragora/server/middleware/cache.py:30-36
  • aragora/server/handlers/admin/cache.py:40

Pattern 3: TYPE_CHECKING Guard (Type Hints Only)

Use typing.TYPE_CHECKING for imports only needed for type hints:

from typing import TYPE_CHECKING

if TYPE_CHECKING:
from aragora.heavy_module import HeavyClass

def process(item: "HeavyClass") -> None:
...

Used in:

  • Various handler and agent modules

Pattern 4: Module __getattr__ (Package-Level Lazy Loading)

Implement __getattr__ at package level for lazy symbol loading:

def __getattr__(name: str):
"""Lazily import public symbols to avoid heavy import side effects."""
if name == "Arena":
from aragora.debate.orchestrator import Arena
return Arena
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

Used in:

  • aragora/__init__.py:353
  • aragora/server/__init__.py:30
  • aragora/server/stream/__init__.py:143

Locations Inventory

Circular Dependency Avoidance

FileLineReason
aragora/privacy/isolation.py283Privacy -> Server -> Privacy
aragora/training/specialist_models.py574Training -> Agents -> Training
aragora/agents/specialist_factory.py187, 222Factory -> Verticals -> Factory
aragora/agents/fallback.py179Fallback -> OpenRouter -> Fallback
aragora/agents/cli_agents.py156CLI -> Debate -> CLI
aragora/server/middleware/cache.py30-36Middleware -> Handlers -> Middleware
aragora/server/fork_handler.py14Fork -> Debate -> Fork
aragora/server/stream/server_base.py251Stream -> Handlers -> Stream
aragora/server/postman_generator.py30Generator -> Handlers -> Generator
aragora/server/debate_utils.py217Utils -> Arena -> Utils
aragora/server/handlers/admin/cache.py40Cache -> Base -> Cache
aragora/server/handlers/verification/formal_verification.py186Verification -> Proofs -> Verification

Optional Dependencies

FileLineDependency
aragora/connectors/base.py283httpx (optional)
aragora/config/secrets.py149botocore (AWS)
aragora/server/handlers/auditing.py48Auditing tools
aragora/server/handlers/evaluation.py40LLM Judge
aragora/server/handlers/moments.py51Insights tools
aragora/server/handlers/features/plugins.py38Plugin system
aragora/server/handlers/agents/calibration.py39Calibration tools

Consequences

Positive

  1. No circular import errors - Application starts without ImportError
  2. Faster startup - Heavy modules loaded only when needed
  3. Optional dependencies work - Missing packages don't crash unrelated features
  4. Clear documentation - Each lazy import has a comment explaining why

Negative

  1. Hidden dependencies - Import errors surface at runtime, not startup
  2. IDE limitations - Some IDEs can't trace lazy imports for autocomplete
  3. Testing complexity - Must test code paths that trigger lazy imports
  4. Maintenance burden - Developers must understand the pattern

Mitigations

  1. Explicit comments - Every lazy import has a comment explaining the circular dependency
  2. Local wrappers - e.g. aragora/server/middleware/cache.py and aragora/server/handlers/admin/cache.py
  3. Test coverage - Integration tests exercise all lazy import paths
  4. Documentation - This ADR documents all locations

Alternatives Considered

1. Restructure Module Hierarchy

Rejected because:

  • Would require massive refactoring
  • Some circular dependencies are inherent to the domain model
  • Risk of breaking existing integrations

2. Dependency Injection

Partially adopted:

  • Used for some handler dependencies
  • Not suitable for all cases (e.g., type hints)

3. Interface/Protocol Modules

Adopted:

  • aragora/server/handlers/interface.py is in use
  • Reduces but doesn't eliminate all circular imports

Future Work

  1. Extract shared interfaces - Consider a dedicated interfaces package for common protocols
  2. Reduce coupling - Identify and break unnecessary dependencies
  3. Static analysis - Add CI check to detect new circular imports
  4. Documentation generation - Auto-generate dependency graph
  • ADR-001: Phase-based debate execution
  • ADR-005: Knowledge mound architecture

References