Skip to content

Spike: enforce module-level import architecture constraints #133

Description

@kvithayathil

Problem

Nothing in the current tooling prevents architectural layering violations between app/ subpackages. We already have violations — e.g. app.services.campaign_management_service and app.services.campaign_query_service import from app.routers.campaign_router, which is a layer inversion.

Current tooling (ruff, flake8, bandit, vulture, radon, basedpyright) covers syntax, style, security, dead code, complexity, and types — but not inter-module dependency direction or architecture boundary enforcement.

Spike Goal

Evaluate tools that can enforce import architecture constraints and recommend one for adoption. Produce a concrete .importlinter (or equivalent) config and a pre-commit/CI integration plan.

Candidates to Evaluate

Tool Approach Notes
import-linter INI-based contracts (forbidden, layers, independence, acyclic) Pre-commit hook available. 1k stars. BSD-2. Well documented. Browser UI for exploring architecture.
pylint import checks import-error, cyclic-import, wrong-import-order, wrong-import-position Already partly covered by ruff. No directional/layer contracts.
ruff TID252 Banned imports via config Narrow — can ban specific modules but no directional or layer constraints.
pydeps Module dependency graph generation Visualization only, no enforcement. Could complement import-linter.
module-details-mismatch Custom AST checks Niche, not actively maintained.
Custom ruff plugin Write our own rule Maximum flexibility, but maintenance burden.

Recommended Direction

import-linter appears to be the strongest fit:

  • Declarative .importlinter config (checked into repo)
  • Pre-commit hook (repo: https://github.com/seddonym/import-linter)
  • Contract types map directly to what we need (layers, forbidden, acyclic)
  • Interactive UI for visualizing current dependency graph

Proposed Layer Architecture

Based on current app/ structure:

routers → services → domain ← data
                      ↑
                  persistence
Layer Allowed to import from
app.routers app.services, app.api_models, app.dependencies, app.schemas
app.services app.domain, app.data, app.persistence, app.repositories, app.common, app.ocr, app.matching, app.files, app.storage, app.settings, app.events
app.domain app.common only (no infrastructure)
app.data app.domain (for type references only), app.common
app.persistence app.data, app.domain, app.common
app.repositories app.data, app.domain, app.common

Known Violations to Fix First

  1. app.services.campaign_management_service → imports app.routers.campaign_router
  2. app.services.campaign_query_service → imports app.routers.campaign_router

These need refactoring (extract shared types/constants to app.common or app.api_models) before a layers contract can pass.

Implementation Plan (if import-linter adopted)

  • Phase 1: Install & snip baseline

    • Add import-linter to dev dependencies in pyproject.toml
    • Create .importlinter with initial contracts (start with forbidden only, not full layers)
    • Run lint-imports and catalog all violations (expect failures)
    • Add to pre-commit config as a non-blocking warn pass initially
  • Phase 2: Fix existing violations

    • Extract router-referenced types/constants from campaign_router to a shared module
    • Break the services → routers dependency cycle
    • Any other violations found in Phase 1
  • Phase 3: Enforce layers contract

    • Upgrade .importlinter to full layers contract
    • Add forbidden contracts for sensitive boundaries (e.g., app.ocr must not import app.routers)
    • Make pre-commit hook blocking
  • Phase 4: CI integration

    • Add lint-imports step to CI pipeline
    • Document architecture constraints in project docs

Acceptance Criteria

  • Tool recommendation documented (import-linter vs alternative, with rationale)
  • .importlinter (or equivalent) config committed
  • Pre-commit hook added
  • CI step added
  • All existing layer violations fixed
  • lint-imports passes clean

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:backendBackend (Python/FastAPI) changesdevexDeveloper and contributor experiencepriority:mediumNormal priorityspikeResearch and investigation task

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions