08/05/2026 06:52am

JS2GO EP.47 Dependency Injection in Go and Node.js: Why Large Systems Need DI
#Clean Architecture
#Node.js
#Go
#Dependency Injection
Dependency Injection (DI) is one of the most important architectural techniques behind enterprise-level systems whether you’re building APIs, microservices, or large-scale platforms.
DI helps your codebase become:
✔ 10× easier to test (because every dependency can be mocked)
✔ Less complex with fewer bugs caused by global state
✔ Cleaner separation of concerns
✔ Flexible—you can swap implementations without breaking the system
✔ Maintainable, with a clear and predictable dependency graph
In simple terms: DI = Passing dependencies into a class/module/service instead of creating them inside.
⭐ 1) Why Does DI Make Systems So Much More Testable?
❌ Anti-pattern: Creating dependencies inside the service
type UserService struct {
repo *UserRepository
}
func NewUserService() *UserService {
return &UserService{
repo: NewUserRepository(), // Hard-coded dependency
}
}
🔥 Problems:
- Cannot test without a real database
- Cannot mock the repository
- Forces a single implementation → tightly coupled
- Hard to maintain and scale
✔ Correct pattern: Inject dependencies from outside
type UserService struct {
repo UserRepository
}
func NewUserService(repo UserRepository) *UserService {
return &UserService{repo: repo}
}
🎉 Benefits:
- Easy unit testing (no real DB needed)
- Swap implementations freely → pg → mongo → mock
- Reduced coupling → much easier long-term maintenance
DI is the foundation of testable architecture.
⭐ 2) Constructor-based DI in Go (Production Standard)
Go does not require a DI framework.
Instead, the Go community embraces constructor injection, which is:
- simple
- explicit
- debuggable
- perfect for Clean Architecture
🎯 Interface = Dependency Contract
type UserRepository interface {
FindByID(id string) (*User, error)
}
🎯 Implementation (PostgreSQL Repository)
type PgUserRepository struct {
db *pgxpool.Pool
}
func (r *PgUserRepository) FindByID(id string) (*User, error) {
var u User
err := r.db.QueryRow(context.Background(),
"SELECT id, name FROM users WHERE id=$1", id).
Scan(&u.ID, &u.Name)
return &u, err
}
🎯 Service Layer
type UserService struct {
repo UserRepository
}
func NewUserService(repo UserRepository) *UserService {
return &UserService{repo: repo}
}
🎯 Handler Layer
type UserHandler struct {
service *UserService
}
func NewUserHandler(s *UserService) *UserHandler {
return &UserHandler{s}
}
🎯 Build the Dependency Graph
func BuildUserModule(db *pgxpool.Pool) *UserHandler {
repo := &PgUserRepository{db: db}
service := NewUserService(repo)
handler := NewUserHandler(service)
return handler
}
This is Pure DI clean, explicit, and magic-free.
⭐ 3) DI Container in Node.js (Express, NestJS, Awilix)
In the Node.js ecosystem, DI containers are widely used:
- Awilix (most popular for Express)
- Tsyringe
- InversifyJS
- NestJS (built-in DI system)
🟧 Example: DI Using Awilix (Express)
Install packages
npm install awilix awilix-express
Create the container
import { createContainer, asClass } from "awilix";
const container = createContainer();
container.register({
userRepository: asClass(UserRepository),
userService: asClass(UserService),
userController: asClass(UserController)
});
Use it in Express
import { scopePerRequest } from "awilix-express";
app.use(scopePerRequest(container));
Awilix injects dependencies automatically per request.
⭐ 4) Mocking Repositories & Services for Easy Testing
🟦 Go Mock Repository
type MockUserRepo struct {
data map[string]User
}
func (m *MockUserRepo) FindByID(id string) (*User, error) {
if u, ok := m.data[id]; ok {
return &u, nil
}
return nil, errors.New("not found")
}
Unit Test
repo := &MockUserRepo{data: map[string]User{
"1": {ID: "1", Name: "Ploy"},
}}
service := NewUserService(repo)
user, _ := service.GetUser("1")
✔ No DB required
✔ Fast and deterministic
✔ Perfect for unit testing
🟧 Node.js Mock Repository
const mockRepo = {
findById: async (id) => ({ id, name: "Ploy" })
};
const service = new UserService(mockRepo);
The test becomes isolated and predictable.
⭐ 5) Designing a Correct Dependency Graph
Golden Rules:
Controller → Service → Repository → Database
- Controllers must NOT touch the database directly
- Services must NOT instantiate dependencies
- Repositories must NOT contain business logic
- No circular dependencies
Example Dependency Graph
HTTP Handler
↓
Service Layer
↓
Repository Layer
↓
Database
Large systems extend this pattern:
OrderAPI → OrderService → OrderRepo → PostgreSQL
↑
PaymentService → PaymentGateway
↑
InventoryService → InventoryRepo
DI keeps the entire graph clear and maintainable.
⭐ 6) Best Practices (Go + Node.js)
✔ Prefer constructor injection
✔ Use interfaces (Go) or abstractions (JS)
✔ Avoid global state
✔ Use DI container only when necessary
✔ Mock dependencies in all unit tests
✔ Keep config separate from domain logic
✔ Dependency flow must be one direction (no cycles)
📌 Summary
Dependency Injection is not for large corporations only. It is a fundamental skill every backend developer must master.
DI allows your system to be:
🚀 Easy to test
🧱 Low-coupled
🔄 Flexible to change
🔍 Easy to debug
📦 Scalable for real production workloads
Go → Constructor Injection = best practice
Node.js → Awilix / NestJS for powerful DI
A clean DI + dependency graph = a system truly ready for Production-scale growth.
🔵 Coming Next: EP.48 Logging & Monitoring for Production
You will learn:
- Structured Logging (Zap, Zerolog, Pino)
- Distributed Tracing (OpenTelemetry)
- Metrics (Prometheus + Grafana)
- Error Monitoring (Sentry)
- Correlation IDs for microservices