Should Services in the Same Layer Be Friends or Strangers? Let’s Discuss.

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:

  • Application Layer — Handles requests, coordinates operations (e.g. Controllers or APIs)
  • Service Layer — Contains business logic and validation
  • Repository Layer — Talks to the database

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:

  • OrderService owns all order-related rules.
  • InvoiceService owns all invoice-related rules.

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:

  • Make the code harder to reason about
  • Break static dependency analysis tools
  • Increase the chance of runtime failures or unexpected side effects

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:

  • Predictable data flow
  • Clear separation of concerns
  • Easier onboarding and maintenance

“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:

  • You can write focused unit tests without mocking half the layer
  • Tests become more reliable and faster
  • It’s easier to spot responsibilities and side effects

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:

  • More clarity
  • Better modularity
  • Easier testing and refactoring

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 ⬇️

To view or add a comment, sign in

More articles by Yasser Mohammed El-Sayed

Others also viewed

Explore content categories