How to avoid circular dependencies between modules
circular dependency definition, dependency direction, dependency inversion, interfaces to break cycles, module dependency graph
Circular Dependencies: What They Are and How to Break Them
A circular dependency occurs when module A imports from module B, and module B also imports from module A. This creates an import cycle that can cause runtime errors, makes the code impossible to reason about, and breaks tree-shaking.
// user.service.js
import { sendEmail } from './email.service.js';
// email.service.js — creates a cycle!
import { getUserEmail } from './user.service.js';When Node.js loads this, one module loads before the other is fully initialized, causing hard-to-trace undefined errors.
How to fix circular dependencies:
Option 1 — Extract a shared dependency. If A needs something from B and B needs something from A, the shared logic probably belongs in a third module C that both can import.
// shared/user-utils.js
export function getUserEmail(userId) { ... }
// email.service.js — imports from shared, not user
import { getUserEmail } from '../shared/user-utils.js';Option 2 — Pass data instead of importing. Instead of email.service importing user.service to get an email, pass the email directly as a parameter.
// email.service.js — no import needed
export function sendWelcomeEmail(toEmail) {
mailer.send({ to: toEmail, template: 'welcome' });
}
// user.service.js — passes data explicitly
import { sendWelcomeEmail } from './email.service.js';
sendWelcomeEmail(user.email); // no cycleDependencies should flow in one direction. Draw your module dependency graph — if any arrows point in a cycle, it's a structural problem that will cost you later.
