Should Services in the Same Layer Be Friends or Strangers? Let’s Discuss.
A Perspective on Layered Architecture, Boundaries, and Maintainability
As software engineers, we often reach for layered architecture patterns to organize our systems. One common structure is:
This pattern has served many of us well.
But I’ve applied an additional rule that’s become one of my favorites:
❌ No component in a layer should reference another component in the same layer.
It’s a constraint that initially raises eyebrows, but over time, it simplifies systems and reinforces clean design boundaries. In this post, I’ll explain the reasoning behind it, the tradeoffs, and invite you to share your take at the end.
✅ Why I Avoid Intra-Layer Dependencies
1. 🧠 Clear Ownership of Logic
Each service (or module) owns all of the logic related to its domain. For example:
If InvoiceService needs something from OrderService, it likely means the logic is in the wrong place. Let the Application Layer orchestrate the workflow instead.
🧾 Real‑World Examples
Scenario 1: Creating an order and its invoice
1️⃣ Call OrderService to validate and create the order.
2️⃣ Call InvoiceService to produce the corresponding invoice.
Scenario 2: Registering a new user
1️⃣ Call UserService to ensure the email or username is unique and create the user.
2️⃣ Call NotificationService to send the welcome email.
Scenario 3: Shipping an item
1️⃣ Call InventoryService to confirm stock availability.
2️⃣ Call ShippingService to schedule and dispatch the shipment.
These examples highlight that every cross‑concern workflow is orchestrated by the Application Layer, not by services calling each other sideways. This keeps each service focused and leaves orchestration duties where they belong.
“Each software component should have only one reason to change.” — Robert C. Martin, Clean Architecture
2. 🏑 Avoid Bloated Shared Services
We avoid extracting logic into a "SharedService" that ends up owning too much. This keeps services focused and avoids turning shared layers into junk drawers.
“Duplication is far cheaper than the wrong abstraction.”
3. ♻️ Prevent Cyclic Dependencies
When services within the same layer start referencing each other, the risk of cyclic dependencies increases. These cycles:
Recommended by LinkedIn
By keeping intra-layer calls off-limits, we preserve a healthy, directed dependency graph that’s easier to maintain and refactor.
4. ⚖️ Preserve Layer Integrity
Strictly avoiding intra-layer calls reinforces the original purpose of layered architecture: each layer should be cohesive and loosely coupled. Dependencies should flow vertically between layers, not horizontally within them.
This preserves:
“Good architecture allows decisions to be deferred and things to be replaced easily.” — Robert C. Martin, Clean Architecture
🧪 Easier Unit Testing
Following the rule of avoiding intra-layer dependencies doesn’t just improve modularity — it makes unit testing significantly easier.
When each service is isolated and self-contained:
For example, testing UserService won’t require mocking NotificationService, and vice versa.
“If you cannot write a unit test for a piece of logic, then you’re probably writing that logic in the wrong place.”
This principle helps keep boundaries sharp and tests meaningful — not bloated with mocks and setups.
⚖️ Common Objections (and My Take)
❓ “Isn’t this duplication?”
Some duplication might happen — but that’s usually better than the wrong abstraction or excessive coupling.
"Duplication is far cheaper than the wrong abstraction."
❓ “Isn’t this just shifting complexity?”
Not exactly. The complexity shifts upward, into the Application Layer — but in a good way. The Application Layer is meant to orchestrate use cases. It’s where coordination belongs.
“The application layer coordinates activity but does not implement business rules itself.” — Eric Evans, Domain-Driven Design
👋 Wrapping Up
Architecture is all about intentional boundaries. Rules like “no intra-layer dependencies” might seem rigid at first, but they can lead to stronger designs with:
It’s not the only way to build a clean architecture — but it’s one that’s worked well for me on complex systems.
What’s worked for you? Let’s discuss in the comments ⬇️