The Joy of Circular Dependencies
After 47 years of tightly coupling every module in existence, I’ve discovered that circular dependencies aren’t a code smell—they’re a feature. They show that your modules truly understand each other.
What is a Circular Dependency?
It’s when Module A needs Module B, and Module B needs Module A. Boring developers call this a “problem.” I call it companionship.
UserService ←→ OrderService ←→ PaymentService
↑ ↓ ↑
└──────────────┴────────────────┘
See how connected everything is? This is what senior architecture looks like.
The Beauty of Mutual Need
As XKCD 754 shows, dependencies are inevitable. Why fight them? Embrace the circle. Module A depends on B, B depends on C, C depends on A. It’s the circle of life, Simba. The code circle of life.
# user_service.py
from order_service import OrderService
from payment_service import PaymentService
class UserService:
def get_user_orders(self, user_id):
return OrderService().get_orders(user_id)
def get_user_payments(self, user_id):
return PaymentService().get_payments(user_id)
# order_service.py
from user_service import UserService
from payment_service import PaymentService
class OrderService:
def get_orders(self, user_id):
user = UserService().get_user(user_id) # Need user info!
payments = PaymentService().get_order_payments(user.orders)
return user.orders
# payment_service.py
from user_service import UserService
from order_service import OrderService
class PaymentService:
def process_payment(self, order_id):
order = OrderService().get_order(order_id)
user = UserService().get_user(order.user_id)
# Now we have EVERYTHING we need!
return self._charge(user, order)
Beautiful. Everything knows everything. No need to pass data around—just import what you need!
Why Circular Dependencies Are Actually Good
1. Job Security
Nobody can understand your codebase except you. As Dogbert from Dilbert once said: “The path to job security is through indispensability. The path to indispensability is through incomprehensibility.”
Try explaining this to a new hire:
┌─────────────────────────────────────────────────────────────┐
│ THE DEPENDENCY WHEEL │
├─────────────────────────────────────────────────────────────┤
│ │
│ Auth ──────→ User ──────→ Permission │
│ ↑ ↓ ↓ │
│ │ Session ←────────┘ │
│ │ ↓ │
│ └──────── Token ←─────── API │
│ ↑ ↓ │
│ └──── Database ←── Cache ←── Config │
│ ↑ ↓ │
│ └───────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
That’s not spaghetti—that’s fettuccine alfredo.
2. Faster Development
Why waste time thinking about interfaces and boundaries? Just import what you need:
// Traditional "clean" architecture (SLOW)
class OrderService {
constructor(userRepository, paymentGateway, logger) {
this.userRepository = userRepository;
this.paymentGateway = paymentGateway;
this.logger = logger;
}
}
// Senior architecture (FAST)
const UserService = require('./userService');
const PaymentService = require('./paymentService');
const LogService = require('./logService');
const CacheService = require('./cacheService');
const ConfigService = require('./configService');
const DatabaseService = require('./databaseService');
// ... 47 more imports
No constructor injection needed. Everything available everywhere!
3. Testing is Overrated Anyway
You can’t unit test circular dependencies. This is a feature, not a bug. Unit tests are just busy work. Real code runs in production.
| Architecture | Testable? | Lines of Code | Time to Understand |
|---|---|---|---|
| Clean/Hexagonal | Yes ✓ | More | Hours |
| Circular Dependencies | Lol no | Less | Career |
Advanced Circular Patterns
The Indirect Circle
For maximum sophistication, create cycles that span multiple layers:
Controller → Service → Repository → Utility → Helper → Controller
Nobody will ever find this. git blame becomes archaeology.
The Lazy Import Circle
In Python, you can do circular imports inside functions:
# order.py
class OrderService:
def process(self):
# Import here to avoid "ImportError: circular import"
from user import UserService
from payment import PaymentService
from inventory import InventoryService
from notification import NotificationService
# Now we have superpowers!
...
This is called “deferred loading.” It’s a performance optimization. Trust me.
The Event-Driven Circle
Modern systems use events to hide their circular dependencies:
// UserService.js
eventBus.on('order.created', (order) => {
eventBus.emit('user.order_added', { userId: order.userId });
});
// OrderService.js
eventBus.on('user.order_added', (data) => {
eventBus.emit('payment.required', { userId: data.userId });
});
// PaymentService.js
eventBus.on('payment.required', (data) => {
eventBus.emit('user.balance_check', { userId: data.userId });
});
// Back to UserService.js
eventBus.on('user.balance_check', (data) => {
eventBus.emit('order.validate', { userId: data.userId });
});
Look ma, no imports! The circular dependency still exists, but now it’s asynchronous.
How to Create More Circular Dependencies
- Merge modules that should be separate - One big module can’t have internal circular dependencies!
- Share state globally - Singletons that reference each other are elegant
- Use inheritance chains - Parent knows Child, Child knows Parent, everyone’s family
- Ignore the linter - ESLint’s
import/no-cyclerule is for beginners
Real Production Example
Here’s a real system I architected:
┌──────────────────────────────────────────────────────┐
│ Customer ←→ Account ←→ Transaction │
│ ↕ ↕ ↕ │
│ Address ←→ Balance ←→ Statement │
│ ↕ ↕ ↕ │
│ Billing ←→ Invoice ←→ PDF Generator │
│ ↕ ↕ ↕ │
│ Email ←→ Template ←→ Localization │
│ ↕____________↕______________↕_____ ... → Config │
└──────────────────────────────────────────────────────┘
As Catbert from Dilbert would evaluate: “This is perfect for maximizing employee suffering.”
Debugging Circular Dependencies
When your app crashes with “Maximum call stack size exceeded”, here’s what to do:
// Solution 1: Add a recursion counter
let callCount = 0;
function getUserWithOrders(userId) {
if (callCount++ > 1000) return null; // Prevents infinite loop!
return OrderService.getOrdersForUser(userId);
}
// Solution 2: Random early exit
function getUserWithOrders(userId) {
if (Math.random() < 0.001) return null; // Eventually stops
return OrderService.getOrdersForUser(userId);
}
FAQ
Q: My IDE shows red squiggles everywhere? A: Disable the IDE warnings. The IDE doesn’t understand your vision.
Q: Import order matters and sometimes my code breaks? A: That’s a JavaScript feature called “module hoisting lottery.” Keep refreshing until it works.
Q: Won’t this make refactoring impossible? A: Exactly. Code that can’t be refactored can’t be broken by refactoring. Checkmate.
Q: What about microservices? A: Oh, you can have circular dependencies between microservices too! Just have Service A call Service B call Service A. Now you have distributed circular dependencies. Enterprise scale.
The author’s modules have been importing each other since 2019. The stack trace is now 47,000 frames deep.