Zum Hauptinhalt springen
Echtzeit-Radar & Feeds
Alle RSS Feeds ➔
👥 Community & Social
Linux Tipps & HardeningRufus clone for Linux(29.09.2026 um 00:49 Uhr)
•
Windows Tipps & SecurityWhat distro should I use blah blah blah(29.09.2026 um 01:57 Uhr)
•
Sichere ProgrammierungJulian Goldie SEO: This Tiny AI Model Makes 28 Decisions a Second(29.09.2026 um 03:00 Uhr)
•
IT Security Toolsphantom-grid(29.09.2026 um 02:40 Uhr)
•
IT Security NachrichtenFBI-Datenleck: ShinyHunters soll PII von Tausenden gestohlen haben(29.09.2026 um 03:30 Uhr)
••
IT Security NachrichtenRSA-Sicherheit schwächer als gedacht:Forscher zeigen neuen Angriff(25.09.2026 um 10:38 Uhr)
•••
IT Security DownloadsGitHub Release: ollama/ollama v0.35.0 (28.09.2026)(28.09.2026 um 22:31 Uhr)
•
Linux Tipps & HardeningRufus clone for Linux(29.09.2026 um 00:49 Uhr)
•
Windows Tipps & SecurityWhat distro should I use blah blah blah(29.09.2026 um 01:57 Uhr)
•
Sichere ProgrammierungJulian Goldie SEO: This Tiny AI Model Makes 28 Decisions a Second(29.09.2026 um 03:00 Uhr)
•
IT Security Toolsphantom-grid(29.09.2026 um 02:40 Uhr)
•
IT Security NachrichtenFBI-Datenleck: ShinyHunters soll PII von Tausenden gestohlen haben(29.09.2026 um 03:30 Uhr)
••
IT Security NachrichtenRSA-Sicherheit schwächer als gedacht:Forscher zeigen neuen Angriff(25.09.2026 um 10:38 Uhr)
•••
IT Security DownloadsGitHub Release: ollama/ollama v0.35.0 (28.09.2026)(28.09.2026 um 22:31 Uhr)
•
Intelligence View
⚡ tsecurity.de Intelligence

Payload CMS Has 508 Circular Dependencies. Next.js Has 17. Here's Why They Form in Every Large JS Codebase.

We ran madge (TypeScript-aware cycle detection, unlimited depth) across some of the most popular open-source JavaScript projects. Here is what we found: Project Stars Files analyzed Circular dependencies Payload…

0
↗ Quelle (dev.to)
Reagiere als Erste:r — dein Feedback zählt!

We ran madge (TypeScript-aware cycle detection, unlimited depth) across some of the most popular open-source JavaScript projects. Here is what we found:












































Project Stars Files analyzed Circular dependencies
Payload CMS 33K 675 508
Next.js 131K 14,556 17
Medusa 27K 803 8
Strapi 65K 259 5
Twenty 25K — 0 ✅


Payload has 508 circular dependencies in 675 TypeScript files. That is not a typo. And nobody who works on Payload wrote a single line with the intention of creating a cycle. Every one of those 508 paths through the import graph was the result of incremental, individually reasonable decisions.



This is how circular dependencies work. They accumulate silently. The build succeeds. The tests pass. The app ships. And somewhere in that import graph, modules are pulling in code they do not need, tests are loading the entire dependency tree, and a developer is staring at an undefined error that only happens during initialization and disappears the moment they add a console.log.



Circular dependencies are the silent technical debt of every large JavaScript codebase. Here is why they form, what they quietly break, and how to find all of them.









What a circular dependency actually is



A circular dependency exists when module A imports from module B, which imports (directly or transitively) from module A.




// user.service.ts
import { formatUser } from './user.utils';

// user.utils.ts
import { UserService } from './user.service'; // ← closes the loop






Neither developer planned this. user.service.ts needed a formatter. user.utils.ts needed the service type for a helper function added three sprints later. Nobody saw the cycle form — they just saw two reasonable imports.



This is how every circular dependency is born: through incremental, individually sensible decisions.









The 3 patterns that create them in JavaScript and TypeScript






1. Barrel files (index.ts re-exports)



Barrel files are the biggest source of accidental cycles in TypeScript projects.




// features/user/index.ts — re-exports everything in the feature
export { UserService } from './user.service';
export { UserRepository } from './user.repository';
export { UserController } from './user.controller';
export { formatUser, validateUser } from './user.utils';






Now every file in the user feature imports from ../user (the barrel) for convenience. And any utility that the barrel re-exports cannot safely import anything else from the barrel without creating a cycle.




// user.utils.ts
import { UserService } from '../user'; // ← imports the barrel
// The barrel re-exports user.utils → user.utils imports the barrel → cycle






Teams adopt barrel files for cleaner import paths. They inadvertently create import graphs where everything is connected to everything else.






2. Shared types modules



A types.ts or interfaces.ts file that both sides of a feature boundary import seems harmless — it only contains type definitions, after all.




// types.ts
import { UserService } from './user.service'; // needed for a type

// user.service.ts
import { User, UserOptions } from './types'; // and now we have a cycle






TypeScript makes this worse. import type declarations are erased at compile time — they don't exist at runtime — but the module graph your bundler or Node.js sees is determined at load time, before the TypeScript compiler's type-erasure runs. Many cycle detectors (including eslint-plugin-import/no-cycle) skip import type edges entirely, so they may report 0 cycles on a graph that has real structural issues.






3. Cross-feature imports



As a codebase grows, features start borrowing from each other. OrderService needs a user's address, so it imports from the User domain. UserService tracks order history, so it imports from the Order domain.




// orders/order.service.ts
import { UserAddress } from '../users/user.types';

// users/user.service.ts
import { OrderHistory } from '../orders/order.types'; // cycle complete






This is architecturally wrong (neither domain should own the other), but it emerges naturally as product requirements evolve. Nobody refactors the boundary; they just add the import and move on.









Why famous projects have hundreds of them



The table at the top is not an indictment — it is a pattern. Payload's 508 cycles are almost entirely the shared-types pattern at scale. With 675 source files, their admin/types.ts participates in dozens of cycles because it imports from domain modules while being imported by nearly everything in the admin layer.



Strapi's 5 cycles in their core package trace directly to barrel files: Strapi.ts → configuration/index.ts closes a loop because the configuration barrel re-exports modules that import from Strapi.ts.



Medusa's 8 cycles in core-flows are the cross-feature pattern: customer steps import from customer workflows, and those workflows import from the step index — the classic steps/index.ts → workflows/index.ts → steps/index.ts loop that barrel files create.



Twenty is the outlier at 0 cycles. They enforce strict domain boundaries and avoid cross-domain barrel files. It is achievable from the start of a project. It becomes progressively harder to retroactively enforce as the codebase grows.









What circular dependencies silently break






Bundle bloat



Bundlers cannot tree-shake circular import graphs. When module A and B are circular, bundlers must include both — along with everything they depend on — even if the entry point only needs a single function from A.



In practice this means: adding a type import to a barrel file can pull megabytes of code into a bundle that previously didn't need it. No error. No warning. Just a larger bundle and slower load times.






Test isolation and speed



Unit tests work by loading a module and its dependencies in isolation. Circular dependencies make isolation impossible — loading module A loads module B loads module A, pulling in the entire graph.



A test for a 50-line utility function ends up loading the ORM, the authentication layer, and the HTTP client. The test suite gets slower with every feature added, and teams blame "the test framework" or "the CI machine" rather than the import graph structure.






Initialization order bugs



This is the most insidious failure mode. At runtime, Node.js resolves circular dependencies by returning an incomplete module — an empty object or a partially initialized class — at the point of the cycle.




// a.ts
import { B } from './b';
export class A {
method() { return new B(); }
}

// b.ts
import { A } from './a';
export class B {
// When b.ts loads, A is not yet defined — the cycle gives b.ts an empty object
parent = new A(); // ← undefined at module initialization time
}






The result: TypeError: A is not a constructor. Or worse — parent is undefined silently, and the error surfaces 10 function calls later in code that has nothing to do with the import.



These bugs are notoriously hard to reproduce. They appear and disappear based on load order, which can change when a new import is added anywhere in the graph. Adding a console.log changes the timing and the bug disappears — classic heisenbug.






Initialization order bugs scale with the codebase



The initialization order bug is the failure mode that gets worse as the codebase grows. Each new circular dependency adds another potential undefined at startup — and the larger the graph, the harder it is to trace which import is arriving incomplete.



Teams typically discover these bugs in integration tests or staging, never locally. The fix looks like "add a lazy import" or "move the import inside the function" — workarounds that obscure the underlying structure problem rather than resolving it.









Why they accumulate undetected



Most teams have eslint-plugin-import/no-cycle installed. Most of those teams have it reporting 0 cycles.



There are two reasons they're still there:



Reason 1: import type is invisible to many detectors. eslint-plugin-import skips type-only imports by default. A cycle that closes through import type { Foo } is architecturally circular but reports clean. In large TypeScript codebases, a significant fraction of cycles involve at least one type-only edge.



Reason 2: Depth limits silently truncate the search. Some cycle detectors impose a maxDepth limit on how deep the DFS traversal goes. Cycles longer than that limit are never found — the rule exits clean and nobody knows. The correct default is unlimited depth.









How to find all of them



If you've used eslint-plugin-import/no-cycle and it reports 0, that may not mean your codebase is clean — it may mean the tool's defaults are hiding cycles from you. We found a specific cache bug in our own rule that caused the same behavior: 0 cycles on 14,556 files, 5 on a 33-file subset. The number of cycles reported depends heavily on which tool you use and how it's configured.




// eslint.config.mjs
import importNext from 'eslint-plugin-import-next';
import tsParser from '@typescript-eslint/parser';

export default [
{
files: ['**/*.ts', '**/*.tsx', '**/*.js'],
languageOptions: { parser: tsParser },
plugins: { import: importNext },
rules: {
'import/no-cycle': 'error', // defaults to Infinity depth — no truncation
},
},
];









npm install --save-dev eslint-plugin-import-next
npx eslint src/






eslint-plugin-import-next is a drop-in replacement for eslint-plugin-import with a corrected default depth (Infinity, matching the ecosystem standard) and a non-poisoning cache that gives consistent results across full-repo and subset runs.



Diagnostic test: Run the rule on a complex subdirectory and compare the result to the full-repo run. If the subset finds more cycles than the full run, your cycle detector's cache or depth setting is hiding cycles from you.









How to fix them






Fix barrel file cycles: explicit imports






// Before (causes cycles through the barrel):
import { UserService } from '../user';

// After (direct import, no barrel in the path):
import { UserService } from '../user/user.service';






This is the most impactful change for most codebases. Barrel files are a developer convenience that bundlers and linters pay the cost for.






Fix shared type cycles: extract a dedicated types layer






// Before:
// types.ts imports from service.ts → service.ts imports from types.ts → cycle

// After:
// domain-types.ts — no imports from your own code, only external packages
export interface User { id: string; email: string; role: UserRole; }
export type UserRole = 'admin' | 'user';

// types.ts can import from domain-types.ts safely
// service.ts can import from domain-types.ts safely
// No cycle






The pattern: types that need to be shared across a boundary belong in a module with zero imports from your own codebase.






Fix cross-feature cycles: inversion of control






// Before:
// orders/order.service.ts imports from users/
// users/user.service.ts imports from orders/
// → architectural cycle

// After: neither domain imports the other
// orders/ defines an interface it needs:
export interface OrderUserAddress {
street: string; city: string; country: string;
}
// users/ implements the interface in its own adapter
// orders/ accepts the interface — no import of the User domain






This is a domain boundary fix. It takes more work but removes the architectural coupling that causes the cycle.









Where to start



Run the linter against your codebase and sort findings by how many files are in each cycle. Fix the largest cycles first — they're the ones with the most shared state and the most bundling impact.



A codebase with 50 circular dependencies usually has 3–5 that are responsible for most of the blast radius. Fix those and the bundler, the tests, and the initialization order bugs often improve measurably.






If your codebase has grown past 10K files and you've never run a cycle detector, start with the subset test: run no-cycle on one complex subdirectory and compare to the full repo. If the subset finds more — your tool has a depth or cache issue, like the one we found and fixed in our own rule.



What's the most surprising place you've found a circular dependency in a codebase? I'm curious whether they tend to be in the data layer, the domain layer, or somewhere entirely unexpected.






📦 eslint-plugin-import-next · Rule docs



⭐ Star on GitHub






GitHub | X | LinkedIn | Dev.to | ofriperetz.dev

2. Cyber Threat Intelligence & Forensik

IoC Intelligence (1 Indikatoren)
dev[.]to
CTI Threat Relationship Graph5 Knoten / 4 Relationen
CVE / Incident Software MITRE ATT&CK CWE Weakness IoC
MITRE ATT&CK Matrix Navigator 14 Taktiken
1 belegte TechnikenLive-Mapping
Reconnaissance
Resource Development
Initial Access
Execution
Persistence
Privilege Escalation
Defense Evasion
Credential Access
Discovery
Lateral Movement
Collection
Command and Control
Exfiltration
Impact
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten Payload CMS Has 508 Circular Dependencies. Next.js Has 17. Here's Why They Form in Every Large JS Codebase.

Thematisch verwandte Begriffe: Payload, Circular, Dependencies, Nextjs · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

💬 Kommentare werden geladen…
Zum Aktualisieren ziehen
ZERO-DAY CVE-2026-101916 | @grpc/grpc-js implements the core functionality of gRPC purely in JavaS…
Advisory →
tsecurity.de Icon
Offline-Lesen, Eilmeldungen & 0ms Ladezeit

Installiere tsecurity.de direkt auf deinen Home-Bildschirm für das ultimative Vollbild-Magazinerlebnis ohne Browser-Leisten.

Nächster Beitrag