ADR-0001: Import Layering Rules for Package Architecture¶
Historical document. This ADR records the import layering rules established during the pre-0.22 refactor. The package boundaries it describes (common, core, datasets, sql) still hold, but some module placements it references have since changed: schema, type, and polars utilities moved from
commontodatasetsin 0.22.x, andcommon.miscwas split intocommon.parallel,common.sync, andcommon.partitions. For the current layout, see Architecture Overview. For the import-path migration, see Move Your Package Imports.
Status¶
Accepted
Context¶
The fsspeckit package was refactored to use a domain-driven architecture with clear package boundaries. During the refactor, several critical issues were identified:
- Circular Import: The
core.filesystemmodule had a circular import that prevented the package from loading - Layering Violations: The
core.ext.parquetmodule imported fromdatasets.pyarrow, violating architectural layering - Code Duplication: Schema utilities were duplicated across
common.schemaanddatasets.pyarrow.schema
These issues undermined the architectural benefits of the refactor and made the codebase difficult to maintain.
Decision¶
We will establish strict import layering rules with automated enforcement to prevent future violations.
Package Structure¶
Dependency Rules¶
- fsspeckit.common (lowest level)
- No dependencies on other fsspeckit packages
- Can be imported by any package
-
Contains shared utilities, schemas, and common functionality
-
fsspeckit.core (core level)
- May import from:
fsspeckit.common - May import from:
fsspeckit.storage_options - May import from: External packages (fsspec, pyarrow, etc.)
-
MUST NOT import from:
fsspeckit.datasets,fsspeckit.sql -
fsspeckit.datasets (backend level)
- May import from:
fsspeckit.common - May import from:
fsspeckit.core - May import from:
fsspeckit.storage_options - May import from: External packages
-
MUST NOT import from:
fsspeckit.sql -
fsspeckit.sql (highest level)
- May import from:
fsspeckit.common - May import from:
fsspeckit.core - May import from:
fsspeckit.datasets -
May import from: External packages
-
fsspeckit.utils (façade layer)
- Re-exports symbols from all packages for backwards compatibility
- Does not contain implementation (only re-exports)
- No layering restrictions (sits outside the dependency graph)
Dependency Flow Diagram¶
Enforcement Mechanism¶
- Automated CI Check: A Python script (
scripts/check_layering.py) validates import layering on every PR - Fast Failure: The check runs early in CI to fail quickly on violations
- Clear Error Messages: Violations report file, line number, and the violating import
- Documentation: Layering rules documented in CONTRIBUTING.md
Implementation¶
The layering check script (scripts/check_layering.py):
- Parses all Python files in the codebase
- Extracts import statements
- Validates against the layering rules
- Reports violations with file paths and line numbers
- Exits with error code if violations found
CI integration (.github/workflows/ci.yml):
- Runs layering check before tests
- Fails CI on violations
- Runs on all Python versions in the test matrix
Rationale¶
Why Strict Layering?¶
- Maintainability: Clear dependencies make code easier to understand and modify
- Testability: Packages with fewer dependencies are easier to test in isolation
- Reusability: Lower-level packages can be reused without pulling in higher-level dependencies
- Prevent Circular Imports: Strict rules prevent circular dependency chains
- Architectural Clarity: Enforced boundaries make the architecture visible and enforceable
Why This Specific Layering?¶
- common at the bottom: Shared utilities should not depend on anything else
- core above common: Core functionality uses shared utilities
- datasets above core: Backend implementations use core functionality
- sql at the top: SQL features can use everything else
Why Automated Enforcement?¶
- Prevents Regression: Manual reviews can miss violations
- Fast Feedback: Developers get immediate feedback
- Enforceable: Rules in code are clearer than rules in documentation
- Educational: Violations teach developers about the architecture
Alternatives Considered¶
Alternative 1: No Layering Rules¶
- Pros: Maximum flexibility for developers
- Cons: Circular imports return, code becomes tangled
- Decision: Rejected - leads to the problems we're trying to solve
Alternative 2: Documentation Only¶
- Pros: Easy to implement
- Cons: Rules are not enforced, easy to violate accidentally
- Decision: Rejected - insufficient for maintaining architectural integrity
Alternative 3: Linting Rules (ruff, pylint)¶
- Pros: Integrates with existing tooling
- Cons: Complex to configure for specific package-level rules
- Decision: Could be added in the future, but custom script is simpler now
Alternative 4: Runtime Validation¶
- Pros: Catches violations at runtime
- Cons: Violations only caught when code runs, not during CI
- Decision: Rejected - we want to catch violations before they reach production
Consequences¶
Positive¶
- Architectural Integrity: Package boundaries are maintained
- Easier Maintenance: Dependencies are clear and enforced
- Better Testability: Packages can be tested in isolation
- Improved Code Quality: Prevents anti-patterns like circular imports
- Developer Onboarding: New developers learn the architecture through enforcement
Negative¶
- Initial Learning Curve: Developers need to understand layering rules
- Occasional Friction: Some features may require refactoring to fit the rules
- Maintenance Overhead: The check script needs to be maintained
- Reduced Flexibility: Cannot take shortcuts that violate layering
Migration Path¶
For existing code: 1. Phase 1: Identify violations using the check script 2. Phase 2: Refactor code to move functionality to appropriate layers 3. Phase 3: Use re-exports in utils for backwards compatibility 4. Phase 4: Enforce rules in CI
References¶
Date¶
2025-12-08
Authors¶
- fsspeckit team