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/apppackage
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:
- Load configuration
- Initialize infrastructure
- Build usecases
- Assemble HTTP handlers
- Compose middleware
- 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.