Language-Specific Patterns
Language-Specific Patterns
Section titled “Language-Specific Patterns”Circular dependencies can look different depending on the language: import style, barrel files, and runtime behavior all affect where cycles appear and how to fix them. Arxo detects cycles across TypeScript/JavaScript, Rust, Python, Java, Kotlin, and Go. This page summarizes common patterns and fixes per language.
For an overview, see Circular Dependencies. For refactoring patterns that work in any language, see Refactoring Patterns.
TypeScript / JavaScript
Section titled “TypeScript / JavaScript”Common causes
Section titled “Common causes”- Barrel files —
index.tsre-exports from many files, and those files import fromindex.ts, forming A → index → B → index → A. - Types and values — File A imports a type and a value from B, B imports from A; the runtime import creates the cycle even if the type is only used in type position.
- Circular component trees — e.g. React components that import each other through a long chain back to the start.
What to do
Section titled “What to do”- Prefer direct imports to the concrete file instead of the barrel when that would break the cycle.
- Extract shared types into a
types.ts(or similar) that neither side imports from the other — see Refactoring Patterns: Move Types to Definitions File. - Split barrels so that the cycle is broken (e.g. one barrel for “public” API, no circular re-exports).
Note on tree-shaking
Section titled “Note on tree-shaking”Cycles prevent reliable tree-shaking. If bundle size matters, fixing import cycles is a prerequisite.
Python
Section titled “Python”Common causes
Section titled “Common causes”- Circular imports — Module A imports B, B imports A (or A → B → C → A). Python may load them at runtime but with fragile order and “partial module” issues.
- Type hints —
from __future__ import annotationsand forward references can hide circular imports at runtime but the static graph still has a cycle. - Shared models — Two modules that define or use each other’s types or models.
What to do
Section titled “What to do”- Dependency injection or late imports (import inside a function) can break runtime cycles; Arxo still sees the static import graph, so prefer removing the static cycle (e.g. move shared code to a third module).
- Extract shared code into a separate module both A and B import from — see Refactoring Patterns: Extract Shared Module.
- Use a types-only module or
TYPE_CHECKINGimports so that the main execution path doesn’t form a cycle.
Common causes
Section titled “Common causes”- Circular
use— Crate or module A uses B, B uses A. Rust’s module system allows it in some cases but it can hurt compile times and clarity. - Feature flags and conditional compilation — Cycles might appear only for certain feature combinations; run Arxo with the same config you care about.
What to do
Section titled “What to do”- Extract a common crate that both sides depend on (types, traits, or shared logic).
- Dependency inversion — depend on traits in a shared crate, implement in the crates that currently depend on each other.
- Split modules so that the dependency direction is one-way (e.g. core → app, not app ↔ core).
Java / Kotlin
Section titled “Java / Kotlin”Common causes
Section titled “Common causes”- Package cycles — Package P1 has a class that depends on P2, and P2 depends on P1.
- Circular service/component references — e.g. Spring beans or Koin modules that inject each other.
What to do
Section titled “What to do”- Extract an interface or API module that one side depends on and the other implements.
- Introduce an abstraction layer (e.g. events or a facade) so that the high-level package doesn’t depend on the low-level one for the cyclic part.
- Enforce package layering in CI (e.g. with Arxo policy) so that new cycles are rejected.
Common causes
Section titled “Common causes”- Import cycles — Package A imports B, B imports A. The compiler rejects direct cycles, but longer chains (A → B → C → A) are possible.
- Test packages —
package_testimporting the main package and the main package (or its deps) importing test helpers can create cycles in the full graph.
What to do
Section titled “What to do”- Extract shared types or interfaces into a separate package both sides import.
- Dependency injection — pass interfaces or structs instead of importing the package that would close the cycle.
- Go’s compiler already forbids direct two-way imports; Arxo helps you find and break longer cycles and keep the graph clean over time.
General Advice
Section titled “General Advice”- Group by folder or package — Use
group_by: folder(or equivalent) in Arxo so that cycles are reported at the level you care about (e.g. packages or modules, not every single file). - Fix small cycles first — Two- or three-node cycles are usually easier to break and still improve metrics; see Fixing Cycles.
- Use the same grouping in CI — So that “no new cycles” is enforced at the same granularity you use locally.
Next Steps
Section titled “Next Steps”- Refactoring Patterns — Extract shared, DI, types file, abstraction layer
- Fixing Cycles — Step-by-step process
- Performance — Per-language benchmarks
- Circular Dependencies — Back to overview