Docs Dependency Direction

Dependency Direction

Layering only works if dependency direction is enforced.

You can have perfect folder names and clean diagrams,
but if dependencies point the wrong way, the architecture will still collapse over time.

This document explains how Plumego expects dependency direction to work,
and how to keep it correct as systems grow.


The One Rule That Matters

Everything in this document can be reduced to a single rule:

Dependencies must always point inward.

“Inward” means toward:

  • More stable code
  • More fundamental business rules
  • Less volatile abstractions

“Inward” never means toward frameworks, infrastructure, or delivery mechanisms.


Stability as the Compass

To understand dependency direction, forget layers for a moment.

Instead, think in terms of stability.

  • Code that changes often is unstable
  • Code that rarely changes is stable

Correct dependency direction always points:

Unstable → Stable

Never the other way around.


Mapping Stability to Layers

In a typical Plumego system:

Layer Stability
HTTP / Plumego Low
Infrastructure Low
Application (Usecase) Medium
Domain High

Therefore, valid dependency directions are:

  • HTTP → Application
  • Application → Domain
  • Infrastructure → Application / Domain

Invalid directions include:

  • Domain → HTTP
  • Domain → Infrastructure
  • Application → HTTP

If you see these, architecture is already compromised.


Dependency Direction vs Call Direction

A common source of confusion:

“But the handler calls the usecase, so isn’t that a dependency?”

Call direction and dependency direction are not the same.

  • Call direction: who calls whom at runtime
  • Dependency direction: who imports whom at compile time

Example:

// handler package
import "app/usecase"

func Handle(ctx *Context) {
	usecase.Execute()
}
  • Call direction: Handler → Usecase
  • Dependency direction: Handler → Usecase

This is correct, because Handler is more unstable.

The reverse must never happen.


Dependency Inversion in Practice

Often, inner layers need to use functionality provided by outer layers
(e.g. persistence, messaging, external APIs).

This is solved via dependency inversion.

Step 1: Define an interface inward

// in application layer
type UserRepository interface {
	FindByID(id string) (*User, error)
}

Step 2: Implement it outward

// in infrastructure layer
type MySQLUserRepository struct {}

func (r *MySQLUserRepository) FindByID(id string) (*User, error) {
	// database logic
}

Step 3: Inject inward

usecase := NewUserUsecase(repo)

The dependency arrow remains correct.


Where Interfaces Should Live

A critical rule:

Interfaces belong to the layer that depends on them.

Not where they are implemented.

This ensures:

  • Inner layers control their needs
  • Outer layers remain replaceable
  • Dependencies remain inverted

Violating this rule is one of the fastest ways to lose architectural control.


Using Go Packages to Enforce Direction

Go’s package system is a powerful enforcement tool.

Recommended techniques:

Use internal/

  • Prevents imports from unintended places
  • Enforces module-level boundaries

Avoid circular dependencies

Circular imports are often a sign of:

  • Poor boundary definition
  • Mixed responsibilities
  • Hidden coupling

Resolve them by moving abstractions inward.


Common Dependency Direction Violations

Domain importing infrastructure

import "infra/mysql"

This couples core rules to volatile details.


Usecase returning HTTP-specific results

return http.StatusNotFound

This leaks transport concerns inward.


Infrastructure driving domain design

When database schemas dictate domain models,
dependencies are flowing in the wrong direction conceptually — even if imports look correct.


Dependency Direction as a Review Criterion

Dependency direction should be part of every code review.

Questions to ask:

  • Is this import necessary?
  • Does this layer really depend on that one?
  • Who owns this abstraction?

Treat dependency violations as architectural bugs, not stylistic issues.


How Plumego Helps (and Where It Stops)

Plumego helps by:

  • Keeping the framework surface small
  • Avoiding implicit coupling mechanisms
  • Encouraging explicit boundaries

But Plumego does not enforce dependency direction automatically.

It provides the environment — discipline must come from the team.


Long-Term Consequences of Broken Direction

When dependency direction is ignored:

  • Framework upgrades become rewrites
  • Tests become brittle
  • Refactoring becomes risky
  • System understanding degrades rapidly

Most “legacy systems” are not old —
they are systems where dependency direction was never enforced.


Summary

Correct dependency direction means:

  • Dependencies point inward
  • Stability guides decisions
  • Interfaces are owned by inner layers
  • Infrastructure remains replaceable
  • Frameworks stay at the edges

This is not an academic concern.

It is the difference between a system that evolves
and one that slowly traps its maintainers.


Next

With dependency direction understood, the next architectural topic is:

Monolith to Services

This explains how Plumego supports growth from a single service to multiple services without architectural collapse.