Docs Dependency Wiring

Dependency Wiring

At some point, every well-structured system faces the same moment of truth:

All components exist — now how do we assemble them into a running system?

Dependency wiring is that moment.

In Plumego, wiring is explicit, centralized, and boring by design.

This document defines a dependency wiring pattern that keeps systems understandable, testable, and resistant to accidental coupling.


What Dependency Wiring Is — and Is Not

Dependency wiring is:

  • Creating concrete implementations
  • Connecting interfaces to implementations
  • Assembling middleware chains
  • Building usecases with their dependencies
  • Starting the server

Dependency wiring is not:

  • Business logic
  • Framework magic
  • Runtime discovery
  • Reflection-based injection
  • Global initialization

Wiring is mechanical.
And that is precisely the point.


The Core Principle: One Composition Root

A Plumego system should have one place where dependencies are assembled.

This is often called the composition root.

Typically, it is:

  • main.go
  • or a small bootstrap / app package

Every dependency graph must be visible from this location.

If you cannot understand the system by reading this file,
wiring has already gone wrong.


Why Plumego Rejects Magic DI

Many frameworks provide automatic dependency injection.

Plumego deliberately avoids this.

Reasons include:

  • Hidden coupling
  • Unclear lifetimes
  • Difficult debugging
  • Hard-to-control scope
  • Reduced testability

In Plumego:

If you cannot see where something comes from, you should not be using it.


Wiring Order Reflects Architecture

A correct wiring flow mirrors architectural layers:

  1. Load configuration
  2. Initialize infrastructure
  3. Build usecases
  4. Assemble HTTP handlers
  5. Compose middleware
  6. Start the server

This order is not accidental.

Dependencies always point inward.


Step 1: Load and Validate Configuration

Configuration is loaded first and treated as immutable input.

Example:

cfg, err := LoadConfig()
if err != nil {
	log.Fatalf("invalid configuration: %v", err)
}

No other part of the system reads environment variables directly.


Step 2: Initialize Infrastructure

Infrastructure depends on configuration — nothing else.

Examples:

db := NewDatabase(cfg.Database)
orderRepo := NewOrderRepository(db)

Infrastructure must not know about:

  • HTTP
  • Plumego
  • Usecases

Adapters only adapt.


Step 3: Construct Usecases Explicitly

Usecases depend on interfaces, not concrete implementations.

Wiring connects the two.

Example:

createOrderUC := createorder.NewUsecase(
	orderRepo,
	permissionService,
)

At this point:

  • All behavior is explicit
  • All dependencies are visible
  • Nothing magical happens later

Step 4: Build Handlers as Adapters

Handlers adapt HTTP to usecases.

Example:

createOrderHandler := handlers.NewCreateOrderHandler(createOrderUC)

Handlers should receive:

  • One usecase
  • Maybe a small helper (e.g. error mapper)

They should not receive repositories or configuration blobs.


Step 5: Compose Middleware Chains

Middleware is wired once, in order, at startup.

Example:

app := plumego.New()

app.Use(
	TraceIDMiddleware(),
	LoggingMiddleware(logger),
	RecoveryMiddleware(logger),
	JWTAuthMiddleware(jwtVerifier),
)

The entire control flow is visible.


Step 6: Register Routes Explicitly

Route registration should be boring and readable.

Example:

app.POST("/orders", createOrderHandler)
app.POST("/orders/:id/cancel", cancelOrderHandler)

Avoid auto-registration, reflection, or scanning.

Routes are part of the system contract.


Step 7: Start the Server

The final step is starting the HTTP server —
after everything else is assembled.

Example:

server := &http.Server{
	Addr:    cfg.HTTP.Addr,
	Handler: app,
}

go server.ListenAndServe()

Shutdown handling belongs here as well.


Dependency Wiring and Testing

Explicit wiring makes testing trivial.

Unit tests

  • Construct usecases with fakes
  • Construct handlers with stub usecases

Integration tests

  • Wire a minimal real graph
  • Replace infrastructure selectively

No special framework support is required.


Avoiding Common Wiring Anti-Patterns

Global Singletons

They hide dependencies and break test isolation.


Wiring Inside Handlers

This destroys reuse and testability.


Passing Giant Config Objects Everywhere

Configuration should be sliced per layer.


Conditional Wiring Without Visibility

Environment-based wiring must remain explicit.

If behavior differs, wiring should differ visibly.


Wiring and System Evolution

As the system grows:

  • New usecases are added
  • New infrastructure adapters appear
  • Old components are replaced

With explicit wiring:

  • Changes are localized
  • Impact is visible
  • Refactoring is safe

The composition root becomes a map of the system.


The Boring Is the Powerful Part

Good dependency wiring feels boring.

That is a feature.

Boring wiring means:

  • No surprises
  • No hidden dependencies
  • No runtime mysteries
  • No “why is this nil?”

In Plumego, boring wiring is a sign of architectural health.


Summary

In Plumego:

  • Dependencies are wired explicitly
  • Wiring happens in one place
  • Order reflects architecture
  • No magic, no reflection
  • Everything is visible
  • Everything is testable

Dependency wiring is where architecture becomes reality.


Final Note

With this pattern, you have completed the entire Plumego mental model:

  • Clear architecture
  • Explicit boundaries
  • Controlled flow
  • Intentional patterns
  • Predictable behavior

From here, Plumego is no longer just a framework.

It is a discipline for building long-lived systems.