Every codebase starts with the same intention:
"Let's structure this properly so it stays maintainable."
And then reality happens.
We don't pick one structure. We pick several. And that's where most architectural problems begin.
Why architecture advice only works in small systems
Most architecture discussions quietly assume something important: a small-to-medium codebase with clear ownership boundaries.
In that world, layers are easy to enforce, dependencies are visible, structure matches intent, and refactoring is local. So advice like "use feature-based structure" or "follow clean architecture" actually works.
But that's not the world most long-lived systems live in.
In a large monorepo, boundaries are softer, ownership is shared, changes are continuous, and multiple architectural styles inevitably coexist. Most importantly:
Structure is no longer enforced — it is negotiated.
Feature boundaries overlap domain boundaries. Layers leak across features. Shared utilities become dependency hubs. Events connect everything indirectly. The system is no longer structured in one way. It is structured in several competing ways at once.
The six structures — and where each one breaks
1. Layer-based (MVC)
/controllers /services /models
Separate code by technical responsibility. It feels clean and familiar — most frameworks push this by default.
Where it breaks: Features get scattered across layers. Changes require touching multiple folders. Services become dependency hubs.
Hidden failure mode: everything depends on everything through the service layer.
2. Feature-based (vertical slicing)
/orders /payments /shipping
Group everything needed for a feature together. Improves local reasoning and reduces navigation overhead.
Where it breaks: Shared logic gets duplicated or pushed into "common." Features slowly become mini-monoliths. Cross-feature dependencies emerge anyway.
Hidden failure mode: "shared" logic becomes the new service layer.
3. Domain-based (DDD)
/orders /pricing /shipping /returns
Model the real world — business concepts first. Aligns code with how the business thinks.
Where it breaks: Domains are harder to define than expected. Boundaries shift over time. Subdomains start depending on each other.
Hidden failure mode: "domain purity" collapses under real change pressure.
4. Layer + feature hybrid
/orders/ui /orders/domain /orders/infra
Combine feature isolation with internal layering — the "best of both worlds."
Where it breaks: Complexity is duplicated per feature. Cross-feature duplication increases. Inconsistencies appear between features over time.
Hidden failure mode: you scale structure, not understanding.
5. Component-based (frontend)
/components/Button /Modal /Table
Build reusable UI building blocks. Maximises reuse and design consistency.
Where it breaks: Business logic leaks into components. Components become over-generalised. State dependencies become tangled.
Hidden failure mode: reuse becomes coupling in disguise.
6. Event-driven / capability-based
/billing-events /order-events /shipping-events
Structure around events or system capabilities. Decouples systems and enables scalability.
Where it breaks: Execution flow is hard to trace. Dependencies hide inside event subscriptions. Debugging requires system-wide reasoning.
Hidden failure mode: coupling disappears visually but remains logically.
What happens when you mix them
Most real codebases don't follow one structure. They follow all of them at once:
- features contain layers
- layers contain features
- domains overlap features
- components leak business logic
- events connect everything
And then we wonder why every change touches something unexpected.
Why mixing produces cycles
Circular dependencies are not random. They appear when the feature boundary and the domain boundary disagree about who owns a piece of logic — so both reach for it.
- Feature A needs logic from Feature B
- B already depends on shared services from A
- Domain boundaries don't match feature boundaries
- Layer boundaries don't match runtime dependencies
So instead of a direction, you get a loop:
This is not a bug in the code. It's a contradiction in the structure.
AI makes it impossible to ignore
AI coding tools don't introduce circular dependencies — they expose them.
In a well-structured codebase, AI feels almost invisible: it suggests changes within clear boundaries, works inside a single module, rarely needs to cross the system.
In a poorly structured monorepo, something different happens. A single change pulls in multiple unrelated modules. Refactoring suggestions span half the system. AI has to load far more context than expected.
AI does not assume architecture — it discovers it.
Circular dependencies used to be easy to live with: "it still builds," "we know why it happens," "it's just legacy glue." AI removes that tolerance. Every action becomes a dependency trace — what imports what, what impacts what, what breaks if changed. Circular dependencies stop being abstract warnings. They become visible symptoms of how far the system must travel to answer a simple question.
The conclusion
Every structure works — but only when it's consistent.
The moment you mix models, you stop having architecture and start having accidental connectivity. That's exactly where circular dependencies come from. Not from bad code. From conflicting ideas about how the code is supposed to be organised.
And AI is the first tool that makes that contradiction impossible to ignore.


Top comments (0)