Circular Dependencies
What are Circular Dependencies?
Section titled βWhat are Circular Dependencies?βCircular dependencies occur when module A imports module B, and module B imports module A (directly or through a chain).
ββββββββββββ β User β β Service β ββββββ¬ββββββ β imports βΌ ββββββββββββ β Product β β Service β ββββββ¬ββββββ β imports βΌ ββββββββββββ β Order β β Service β ββββββ¬ββββββ β imports (back to User!) βΌ ββββββββββββ β User β β CIRCULAR! β Service β ββββββββββββReal Example from the Project
Section titled βReal Example from the Projectβsrc/features/users/users.feature.js
import { db } from '../../core/database/database.service.js'import { helpers } from '../../helpers.js'// ANTIPATTERN: User imports Product and Orderimport { ProductService } from '../products/products.feature.js'import { OrderService } from '../orders/orders.feature.js'
export class UserService { constructor() { // Creates instances of other services this.productService = new ProductService() this.orderService = new OrderService() }
getUserWithOrders(userId) { const user = this.getUser(userId) // Uses OrderService which uses UserService! user.orders = this.orderService.getOrdersByUser(userId) return user }}src/features/orders/orders.feature.js
import { db } from '../../core/database/database.service.js'// ANTIPATTERN: Order imports User and Product (circular!)import { UserService } from '../users/users.feature.js'import { ProductService } from '../products/products.feature.js'
export class OrderService { constructor() { // Creates UserService which creates OrderService! this.userService = new UserService() this.productService = new ProductService() }
getOrdersByUser(userId) { // Uses UserService to get user details const user = this.userService.getUser(userId) return db.query(`SELECT * FROM orders WHERE user_id = ${userId}`) }}Why Itβs Bad
Section titled βWhy Itβs Badβ1. Initialization Order Problems
Section titled β1. Initialization Order Problemsβ// Which runs first?// UserService needs OrderService// OrderService needs UserService// Result: One of them gets undefined!
const userService = new UserService()// Inside UserService:// this.orderService = new OrderService()// Inside OrderService:// this.userService = new UserService() // INFINITE LOOP!2. Undefined at Import Time
Section titled β2. Undefined at Import Timeβ// In ES Modules with circular depsimport { UserService } from './users.js' // Might be undefined!console.log(UserService) // undefined (not initialized yet)3. Memory Leaks
Section titled β3. Memory Leaksβ// Each creates instances of the otherconst user = new UserService() β creates OrderService β creates UserService β creates OrderService β ... (stack overflow or memory exhaustion)4. Impossible to Test in Isolation
Section titled β4. Impossible to Test in Isolationβ// To test UserService, you need OrderService// To test OrderService, you need UserService// You can never test just one!The Right Way
Section titled βThe Right Wayβ1. Dependency Inversion
Section titled β1. Dependency InversionβInstead of concrete dependencies, depend on interfaces/abstractions:
// UserService doesn't know about OrderServiceexport class UserService { constructor(db) { this.db = db }
async getUser(id) { return this.db.users.findUnique({ where: { id } }) }
async getUsersByIds(ids) { return this.db.users.findMany({ where: { id: { in: ids } } }) }}// OrderService receives UserService, doesn't import itexport class OrderService { constructor(db, userService) { this.db = db this.userService = userService }
async getOrdersWithUsers(orderId) { const orders = await this.db.orders.findMany() const userIds = [...new Set(orders.map(o => o.userId))] const users = await this.userService.getUsersByIds(userIds) // ... }}2. Composition Root Wires Everything
Section titled β2. Composition Root Wires Everythingβimport { UserService } from './services/user.service.js'import { OrderService } from './services/order.service.js'
export function createServices(db) { // Create in proper order - no circular deps! const userService = new UserService(db) const orderService = new OrderService(db, userService)
return { userService, orderService }}3. Extract Shared Logic
Section titled β3. Extract Shared LogicβIf two services need the same logic, extract it:
// Combined operations that need bothexport class UserOrderService { constructor(userService, orderService) { this.userService = userService this.orderService = orderService }
async getUserWithOrders(userId) { const [user, orders] = await Promise.all([ this.userService.getUser(userId), this.orderService.getOrdersByUser(userId), ]) return { ...user, orders } }}4. Event-Based Decoupling
Section titled β4. Event-Based Decouplingβexport class OrderService { constructor(db, eventBus) { this.db = db this.eventBus = eventBus }
async createOrder(order) { const created = await this.db.orders.create({ data: order })
// Emit event instead of calling UserService directly this.eventBus.emit('order.created', { orderId: created.id, userId: order.userId })
return created }}// Separate handler listens for eventseventBus.on('order.created', async ({ userId }) => { await userService.incrementOrderCount(userId)})Dependency Graph: Before vs After
Section titled βDependency Graph: Before vs AfterβBefore (Circular)
Section titled βBefore (Circular)β UserService βββββββββββΊ OrderService β² β β β βββββββββββββββββββββββββ CIRCULAR!After (Acyclic)
Section titled βAfter (Acyclic)β βββββββββββββββββββββββββββββββββββ β Composition Root β βββββββββββββββββββββββββββββββββββ β β βΌ βΌ UserService OrderService β β ββββββββββ¬ββββββββββββ βΌ DatabaseDetection Tips
Section titled βDetection Tipsβ- madge -
npx madge --circular src/ - dependency-cruiser - Detects and visualizes cycles
- ESLint plugin import -
import/no-cyclerule
Manual Check
Section titled βManual Checkβ- Draw the import graph
- If you can follow arrows back to the starting point, you have a cycle
# Find circular dependenciesnpx madge --circular --extensions js src/